{"record":{"id":"118cddcc155fe479","repo":"astrid-runtime/astrid","slug":"daemon-connection-closed-before-command-result","errorCode":null,"errorMessage":"daemon connection closed before command result","messagePattern":"daemon connection closed before command result","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/capsule_verb.rs","lineNumber":302,"sourceCode":"    client: &mut SocketClient,\n    result_topic: &str,\n    provider: &str,\n    principal: &str,\n    timeout: Duration,\n) -> Result<CommandWait> {\n    let deadline = tokio::time::Instant::now()\n        .checked_add(timeout)\n        .unwrap_or_else(tokio::time::Instant::now);\n    loop {\n        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());\n        if remaining.is_zero() {\n            anyhow::bail!(\"timed out waiting for capsule command result\");\n        }\n\n        let read = tokio::time::timeout(remaining, client.read_raw_frame()).await;\n        let frame = match read {\n            Ok(Ok(Some(bytes))) => bytes,\n            Ok(Ok(None)) => anyhow::bail!(\"daemon connection closed before command result\"),\n            Ok(Err(err)) => return Err(err),\n            Err(_) => anyhow::bail!(\"timed out waiting for capsule command result\"),\n        };\n        let Ok(raw) = serde_json::from_slice::<serde_json::Value>(&frame) else {\n            continue;\n        };\n        let topic = raw.get(\"topic\").and_then(serde_json::Value::as_str);\n        if topic == Some(result_topic) {\n            return Ok(CommandWait::Result(raw));\n        }\n        if topic == Some(CAPSULES_LOADED_TOPIC)\n            && capsules_loaded_missing_provider(&raw, provider, principal)\n        {\n            return Ok(CommandWait::ProviderUnloaded);\n        }\n    }\n}\n","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/capsule_verb.rs#L284-L320","documentation":"While waiting for a capsule command result, read_raw_frame() returned Ok(None), meaning the daemon closed the IPC connection (EOF) before any result frame was delivered. This is thrown instead of an I/O error because a clean EOF is distinct from a transport failure — the daemon (or the socket server) ended the session mid-request.","triggerScenarios":"Calling execute -> wait_for_command_result and the daemon process exits or shuts down its socket accept loop while the command is in flight; the daemon crashes or is killed; the ephemeral daemon's lifetime ends before the result is published.","commonSituations":"Daemon OOM-killed or crashing on a malicious/failing capsule; operator restarting the daemon while a long command runs; ephemeral daemon tied to a parent process that exited.","solutions":["Check the daemon boot/stderr log for a crash (panic, OOM) at the time of the command and fix the underlying capsule fault.","Restart the daemon (`astrid restart`) and re-run the command to confirm it was a transient daemon death.","If the daemon is being shut down intentionally, drain in-flight commands before stopping it.","If a specific capsule reliably crashes the daemon, isolate/reproduce it and file a bug against the daemon's capsule sandboxing."],"exampleFix":"// before: client treats EOF as a bare error\nOk(Ok(None)) => anyhow::bail!(\"daemon connection closed before command result\"),\n// after: caller retries once against a freshly ensured daemon\nmatch wait_for_command_result(&mut client, &topic, provider, principal, timeout).await {\n    Err(e) if e.to_string().contains(\"connection closed\") => {\n        ensure_daemon(label).await?;\n        return execute(provider, verb, args).await; // retry once\n    },\n    other => other.map(|_| ExitCode::from(0)),\n}","handlingStrategy":"try-catch","validationCode":"// Before dispatching, verify the daemon endpoint is accepting connections\nlet outcome = astrid_core::local_transport::connect_outcome(&socket_path).await?;\nif !matches!(outcome, ConnectOutcome::Connected(_)) { anyhow::bail!(\"daemon not accepting connections\"); }","typeGuard":null,"tryCatchPattern":"match wait_for_command_result(&mut client, &topic, provider, principal, timeout).await {\n    Err(e) if e.to_string().contains(\"connection closed before command result\") => {\n        eprintln!(\"daemon died mid-command; inspecting boot log and restarting\");\n        ensure_daemon(\"cli\").await?;\n        // retry the command once against the fresh daemon\n    },\n    other => other,\n}","preventionTips":["Watch the daemon process (PID/supervisor) so crashes are detected and restarted automatically.","Keep ephemeral daemons tied to the correct parent lifetime for long commands.","Tune capsule sandbox limits to avoid OOM-killing the daemon.","Check the boot/stderr log after any EOF to catch repeat crashes early."],"tags":["ipc","connection-closed","daemon","eof"],"backgroundTag":"connection-closed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}