{"record":{"id":"03f13dbddbfc97d6","repo":"astrid-runtime/astrid","slug":"daemon-closed-the-response-stream-before-the-final","errorCode":null,"errorMessage":"daemon closed the response stream before the final marker","messagePattern":"daemon closed the response stream before the final marker","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/agent/spawn.rs","lineNumber":209,"sourceCode":"            reason: Some(\"spawn\".to_string()),\n        },\n        session.0,\n    );\n    let _ = client.send_message(disconnect).await;\n\n    result\n}\n\n/// Read response events until the terminal `AgentResponse { is_final: true }`,\n/// accumulating text. Approval requests are auto-DENIED: a locked-down\n/// throwaway drafts a result for review, it never acts in the world.\nasync fn drain_until_final(client: &mut SocketClient, session: &SessionId) -> Result<String> {\n    let mut response = String::new();\n    loop {\n        let message = match client.read_message().await {\n            Ok(Some(msg)) => msg,\n            Ok(None) => {\n                return Err(anyhow!(\n                    \"daemon closed the response stream before the final marker\"\n                ));\n            },\n            Err(e) => return Err(e.context(\"failed to read from daemon\")),\n        };\n        match &message.payload {\n            astrid_types::ipc::IpcPayload::AgentResponse { text, is_final, .. } => {\n                response.push_str(text);\n                if *is_final {\n                    break;\n                }\n            },\n            astrid_types::ipc::IpcPayload::ApprovalRequired { request_id, .. } => {\n                let deny = astrid_types::ipc::IpcPayload::ApprovalResponse {\n                    request_id: request_id.clone(),\n                    decision: \"deny\".to_string(),\n                    reason: Some(\"spawn: locked-down throwaway never acts\".to_string()),\n                };","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/agent/spawn.rs#L191-L227","documentation":"Raised in drain_until_final when the daemon closes the socket (read_message returns Ok(None)) before emitting the message carrying the final marker. It indicates the response stream terminated prematurely rather than by error message or final completion.","triggerScenarios":"Daemon process crashed or was killed mid-turn, daemon hit a panic while processing the job, socket closed unexpectedly by the OS or an intermediate supervisor, or a daemon version that never emits the final marker for this job type.","commonSituations":"OOM-killed daemon during a large job, daemon restart initiated by a supervisor while a job was in flight, protocol mismatch between an old client and newer daemon.","solutions":["Check daemon logs/crash dumps for the reason the stream ended early","Restart the daemon and retry the job","Align client and daemon versions so the final-marker protocol matches"],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"// preflight: confirm daemon is alive and protocol version matches\nlet ok = client.status_check().await.is_ok();","typeGuard":null,"tryCatchPattern":"match drain_until_final(&mut client, &session).await { Err(e) if e.to_string().contains(\"before the final marker\") => { eprintln!(\"daemon died mid-response; check daemon logs and retry\"); restart_daemon_and_retry().await }, other => other }","preventionTips":["Run the daemon under a supervisor with restart + logging","Keep client and daemon versions in lockstep","Watch for OOM/panics in daemon logs when running large jobs"],"tags":["ipc","protocol","stream"],"backgroundTag":"unexpected-response-shape","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}