astrid-runtime/astrid · error

daemon closed the response stream before the final marker

Error message

daemon closed the response stream before the final marker

What it means

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.

Solutions

  1. Check daemon logs/crash dumps for the reason the stream ended early
  2. Restart the daemon and retry the job
  3. Align client and daemon versions so the final-marker protocol matches

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: confirm daemon is alive and protocol version matches
let ok = client.status_check().await.is_ok();

Try / catch

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 }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/03f13dbddbfc97d6. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/agent/spawn.rs:209

            reason: Some("spawn".to_string()),
        },
        session.0,
    );
    let _ = client.send_message(disconnect).await;

    result
}

/// Read response events until the terminal `AgentResponse { is_final: true }`,
/// accumulating text. Approval requests are auto-DENIED: a locked-down
/// throwaway drafts a result for review, it never acts in the world.
async fn drain_until_final(client: &mut SocketClient, session: &SessionId) -> Result<String> {
    let mut response = String::new();
    loop {
        let message = match client.read_message().await {
            Ok(Some(msg)) => msg,
            Ok(None) => {
                return Err(anyhow!(
                    "daemon closed the response stream before the final marker"
                ));
            },
            Err(e) => return Err(e.context("failed to read from daemon")),
        };
        match &message.payload {
            astrid_types::ipc::IpcPayload::AgentResponse { text, is_final, .. } => {
                response.push_str(text);
                if *is_final {
                    break;
                }
            },
            astrid_types::ipc::IpcPayload::ApprovalRequired { request_id, .. } => {
                let deny = astrid_types::ipc::IpcPayload::ApprovalResponse {
                    request_id: request_id.clone(),
                    decision: "deny".to_string(),
                    reason: Some("spawn: locked-down throwaway never acts".to_string()),
                };

View on GitHub (pinned to affd8760f4)