astrid-runtime/astrid · error
daemon connection closed before command result
Error message
daemon connection closed before command result
What it means
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.
Source
Thrown at crates/astrid-cli/src/commands/capsule_verb.rs:302
client: &mut SocketClient,
result_topic: &str,
provider: &str,
principal: &str,
timeout: Duration,
) -> Result<CommandWait> {
let deadline = tokio::time::Instant::now()
.checked_add(timeout)
.unwrap_or_else(tokio::time::Instant::now);
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
anyhow::bail!("timed out waiting for capsule command result");
}
let read = tokio::time::timeout(remaining, client.read_raw_frame()).await;
let frame = match read {
Ok(Ok(Some(bytes))) => bytes,
Ok(Ok(None)) => anyhow::bail!("daemon connection closed before command result"),
Ok(Err(err)) => return Err(err),
Err(_) => anyhow::bail!("timed out waiting for capsule command result"),
};
let Ok(raw) = serde_json::from_slice::<serde_json::Value>(&frame) else {
continue;
};
let topic = raw.get("topic").and_then(serde_json::Value::as_str);
if topic == Some(result_topic) {
return Ok(CommandWait::Result(raw));
}
if topic == Some(CAPSULES_LOADED_TOPIC)
&& capsules_loaded_missing_provider(&raw, provider, principal)
{
return Ok(CommandWait::ProviderUnloaded);
}
}
}
View on GitHub (pinned to affd8760f4)
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.
Example fix
// before: client treats EOF as a bare error
Ok(Ok(None)) => anyhow::bail!("daemon connection closed before command result"),
// after: caller retries once against a freshly ensured daemon
match wait_for_command_result(&mut client, &topic, provider, principal, timeout).await {
Err(e) if e.to_string().contains("connection closed") => {
ensure_daemon(label).await?;
return execute(provider, verb, args).await; // retry once
},
other => other.map(|_| ExitCode::from(0)),
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before dispatching, verify the daemon endpoint is accepting connections
let outcome = astrid_core::local_transport::connect_outcome(&socket_path).await?;
if !matches!(outcome, ConnectOutcome::Connected(_)) { anyhow::bail!("daemon not accepting connections"); } Try / catch
match wait_for_command_result(&mut client, &topic, provider, principal, timeout).await {
Err(e) if e.to_string().contains("connection closed before command result") => {
eprintln!("daemon died mid-command; inspecting boot log and restarting");
ensure_daemon("cli").await?;
// retry the command once against the fresh daemon
},
other => other,
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- daemon closed guard uplink
- Daemon closed the connection before responding
- daemon metadata lookup failed: {error}
- unexpected daemon metadata response: {other:?}
- daemon rejected capsule metadata request: {message}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/118cddcc155fe479.
Report an issue: GitHub.