astrid-runtime/astrid · error

{primary:#}; additional shutdown failure: {secondary:#}

Error message

{primary:#}; additional shutdown failure: {secondary:#}

What it means

combine_stop_results merges the two halves of `astrid stop` — the MCP gateway shutdown and the daemon shutdown. When BOTH fail, neither failure can be silently dropped, so the primary is reported and the daemon-side failure is appended as 'additional shutdown failure: {secondary:#}' via anyhow's alternate formatting (the full error chain).

Source

Thrown at crates/astrid-cli/src/commands/daemon.rs:728

    println!("{}", theme::Theme::success(message));
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DaemonStopDisposition {
    AlreadyStopped,
    Graceful,
    Forced,
}

fn combine_stop_results(
    gateway: Result<()>,
    daemon: Result<DaemonStopDisposition>,
) -> Result<DaemonStopDisposition> {
    match (gateway, daemon) {
        (Ok(()), Ok(disposition)) => Ok(disposition),
        (Err(primary), Ok(_)) | (Ok(()), Err(primary)) => Err(primary),
        (Err(primary), Err(secondary)) => {
            anyhow::bail!("{primary:#}; additional shutdown failure: {secondary:#}")
        },
    }
}

async fn stop_daemon() -> Result<DaemonStopDisposition> {
    let socket_path = socket_client::proxy_socket_path();
    let pid_path = socket_client::pid_path();

    // Capture the daemon's identity up front: it deletes its own PID file only
    // on a CLEAN exit, so reading it before shutdown is the only reliable way to
    // keep a handle for confirming exit / signalling a wedged shutdown.
    let recorded = daemon_control::read_daemon_identity(&pid_path);
    let socket_present = astrid_core::local_transport::endpoint_is_present(&socket_path)
        .context("failed to inspect daemon endpoint")?;

    // Genuinely nothing running: no socket AND no live recorded process.
    let recorded_alive = recorded
        .as_ref()

View on GitHub (pinned to affd8760f4)

Solutions

  1. Read the primary error first — fix that root cause; the secondary is usually the same underlying problem (e.g. permissions, wedged PID).
  2. If both report a stuck PID, SIGTERM/SIGKILL the recorded daemon PID manually and remove stale runtime files.
  3. Re-run `astrid stop` after cleanup; it is idempotent (AlreadyStopped path).
  4. Check runtime dir ownership/permissions if both failures mention file access.

Example fix

// before
Error: gateway stop failed: ...; additional shutdown failure: shutdown stage daemon.shutdown_ack: rejected: ...
// after
$ kill -TERM <recorded-daemon-pid> && rm -f ~/.astrid/run/system.sock ~/.astrid/run/daemon.pid
$ astrid stop
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

match stop_everything().await {
    Err(e) if e.to_string().contains("additional shutdown failure") => {
        // both legs failed: force-clean the recorded identity then retry once
        force_clean_runtime()?;
        stop_everything().await?;
    }
    r => r?,
}

Prevention

When it happens

Trigger: `astrid stop` (handle_stop) where stop_gateway() and stop_daemon() both return Err — e.g. the socket is unreachable AND the gateway process also fails to shut down (wedged child, permission problem, both holding stale locks).

Common situations: System under resource pressure so both processes are wedged; shared runtime-dir permission problems breaking both shutdown paths; a kill sweep (OOM, container stop) that left both components half-dead; stale PID files confusing both paths.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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