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

  1. Check the daemon boot/stderr log for a crash (panic, OOM) at the time of the command and fix the underlying capsule fault.
  2. Restart the daemon (`astrid restart`) and re-run the command to confirm it was a transient daemon death.
  3. If the daemon is being shut down intentionally, drain in-flight commands before stopping it.
  4. 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

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

Related errors


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