{"record":{"id":"2cd399cbaaad48cd","repo":"Hmbown/CodeWhale","slug":"runtime-event-stream-ended-before-turn-completed","errorCode":null,"errorMessage":"runtime event stream ended before turn.completed","messagePattern":"runtime event stream ended before turn\\.completed","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/app-server/src/lib.rs","lineNumber":1333,"sourceCode":"                                }),\n                            )\n                            .await?;\n                        }\n                    }\n                    \"turn.completed\" => {\n                        let status = turn_terminal_status(&payload);\n                        let error = payload\n                            .pointer(\"/turn/error\")\n                            .and_then(Value::as_str)\n                            .map(str::to_string);\n                        return Ok((last_seq, status, error));\n                    }\n                    _ => {}\n                }\n            }\n        }\n\n        bail!(\"runtime event stream ended before turn.completed\")\n    }\n\n    #[cfg(test)]\n    fn from_base_url_for_test(base_url: String) -> Self {\n        install_rustls_crypto_provider();\n        Self {\n            base_url,\n            client: codewhale_release::platform_http_client_builder()\n                .timeout(Duration::from_secs(5))\n                .build()\n                .expect(\"build reqwest test client\"),\n            auth_token: None,\n            child: None,\n            thread_map: HashMap::new(),\n            last_seq_by_thread: HashMap::new(),\n        }\n    }\n}","sourceCodeStart":1315,"sourceCodeEnd":1351,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/app-server/src/lib.rs#L1315-L1351","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: assuming the stream always yields turn.completed\nlet (seq, status, err) = server.drain_turn_events(...).await?;\n\n// after: treat a premature end as retryable\nmatch server.drain_turn_events(...).await {\n    Ok(outcome) => outcome,\n    Err(e) if e.to_string().contains(\"before turn.completed\") => {\n        let state = server.query_turn_state(&turn_id).await?; // REST fallback\n        state\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"retry","validationCode":"// Prefer REST state over blind streaming when resuming\nif let Some(state) = server.try_query_turn(&turn_id).await? {\n    if state.is_terminal() { return Ok(state); }\n}","typeGuard":null,"tryCatchPattern":"let mut attempt = 0;\nloop {\n    match server.drain_turn_events(&turn_id, since).await {\n        Ok(outcome) => break Ok(outcome),\n        Err(e) if e.to_string().contains(\"before turn.completed\") && attempt < 2 => {\n            attempt += 1;\n            since = last_known_seq;          // replay from what we saw\n            continue;                        // stream was cut, turn may still be live\n        }\n        Err(e) => break Err(e),\n    }\n}","preventionTips":["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."],"tags":["sse","streaming","turn-lifecycle","app-server"],"backgroundTag":"event-stream-truncated","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}