Hmbown/CodeWhale · error · anyhow::Error
runtime event stream ended before turn.completed
Error message
runtime event stream ended before turn.completed
What it means
The app-server drains runtime SSE events waiting for a turn.completed (or terminal-status) event. If the HTTP stream ends (server closes connection, chunk iterator returns None) before any terminal event arrives, this bail fires. The turn's outcome is genuinely unknown — it neither completed nor reported an error.
Source
Thrown at crates/app-server/src/lib.rs:1333
}),
)
.await?;
}
}
"turn.completed" => {
let status = turn_terminal_status(&payload);
let error = payload
.pointer("/turn/error")
.and_then(Value::as_str)
.map(str::to_string);
return Ok((last_seq, status, error));
}
_ => {}
}
}
}
bail!("runtime event stream ended before turn.completed")
}
#[cfg(test)]
fn from_base_url_for_test(base_url: String) -> Self {
install_rustls_crypto_provider();
Self {
base_url,
client: codewhale_release::platform_http_client_builder()
.timeout(Duration::from_secs(5))
.build()
.expect("build reqwest test client"),
auth_token: None,
child: None,
thread_map: HashMap::new(),
last_seq_by_thread: HashMap::new(),
}
}
}View on GitHub (pinned to 0c42157ee5)
Solutions
- Check whether the runtime process is still running and inspect its logs for a crash or panic during the turn.
- Retry the turn: because the stream ended without a terminal event, the previous turn id is not reusable; start a fresh turn.
- If a proxy sits in the middle, raise its read/idle timeout so long turns do not have their SSE connection closed.
- On resume, pass a since_seq older than the terminal event you need so turn.completed is re-delivered, or re-query turn state via the REST API instead of the stream.
Example fix
// before: assuming the stream always yields turn.completed
let (seq, status, err) = server.drain_turn_events(...).await?;
// after: treat a premature end as retryable
match server.drain_turn_events(...).await {
Ok(outcome) => outcome,
Err(e) if e.to_string().contains("before turn.completed") => {
let state = server.query_turn_state(&turn_id).await?; // REST fallback
state
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: retry
Validate before calling
// Prefer REST state over blind streaming when resuming
if let Some(state) = server.try_query_turn(&turn_id).await? {
if state.is_terminal() { return Ok(state); }
} Try / catch
let mut attempt = 0;
loop {
match server.drain_turn_events(&turn_id, since).await {
Ok(outcome) => break Ok(outcome),
Err(e) if e.to_string().contains("before turn.completed") && attempt < 2 => {
attempt += 1;
since = last_known_seq; // replay from what we saw
continue; // stream was cut, turn may still be live
}
Err(e) => break Err(e),
}
} Prevention
- Persist last_seq as events arrive so a reconnect can resume instead of relying on one stream.
- Raise proxy idle timeouts for long turns.
- Watch the runtime process health during long-running turns.
When it happens
Trigger: Runtime process exits or restarts mid-turn; network drop between app-server and runtime closes the response body; runtime closes the SSE connection after keep-alive idle timeout without sending turn.completed; a turn that errors out without emitting a terminal event.
Common situations: Runtime crashed or was killed while a turn was in flight, proxy idle-timeout cutting long-running streams, runtime version that forgets to emit turn.completed on early error paths, or resuming from a since_seq where the terminal event was already emitted before subscription.
Related errors
- runtime SSE frame exceeded {MAX_SSE_FRAME_BYTES} bytes witho
- PythonRuntime::with_state_path is deprecated — use PythonRun
- SSE stream request failed: HTTP {status}: {error_text}
- unrecognized SSE event: {e}
- SSE stream idle timeout after {}s — no data received (bytes_
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/2cd399cbaaad48cd.
Report an issue: GitHub.