astrid-runtime/astrid · error

{primary:#}; additional gateway cleanup failure: {secondary:

Error message

{primary:#}; additional gateway cleanup failure: {secondary:#}

What it means

combine_gateway_results merges the accept-loop result and the gateway cleanup result. If BOTH fail, the error is neither suppressed nor lost: the primary error is re-raised with the secondary cleanup error appended as context, so a cleanup failure cannot mask the real accept failure (or vice versa).

Source

Thrown at crates/astrid-cli/src/commands/mcp/gateway.rs:557

            state.wait_for_stop_acks(),
        )
        .await
        .context("shutdown stage gateway.final_ack_delivery")?;
    }

    let result = combine_gateway_results(accept_result, cleanup_result);
    if result.is_ok() && !control_stop {
        crate::commands::daemon::retire_disconnected_projection(daemon_pid).await?;
    }
    result
}

fn combine_gateway_results(accept: Result<ExitCode>, cleanup: Result<()>) -> Result<ExitCode> {
    match (accept, cleanup) {
        (Ok(exit), Ok(())) => Ok(exit),
        (Err(primary), Ok(())) | (Ok(_), Err(primary)) => Err(primary),
        (Err(primary), Err(secondary)) => {
            anyhow::bail!("{primary:#}; additional gateway cleanup failure: {secondary:#}")
        },
    }
}

async fn accept_loop(
    listener: UnixListener,
    state: Arc<GatewayState>,
    idle_grace: Duration,
) -> Result<ExitCode> {
    let idle = tokio::time::sleep(idle_grace);
    tokio::pin!(idle);
    loop {
        tokio::select! {
            biased;
            () = state.shutdown.cancelled() => return Ok(ExitCode::SUCCESS),
            // Retain and consume the final disconnect before an expired timer.
            () = state.connections_drained.notified() => {
                idle.as_mut().reset(Instant::now().checked_add(idle_grace)

View on GitHub (pinned to affd8760f4)

Solutions

  1. Fix the primary accept-loop error first — the appended message after the ';' is secondary cleanup noise.
  2. If the trailing 'additional gateway cleanup failure' mentions ready/lease/socket files, remove stale files under the daemon_root directory manually and check permissions.
  3. Re-run the gateway after correcting the root cause; this composite error is diagnostic, not a configuration knob.
  4. If cleanup failures recur, verify the daemon_root directory is writable by the gateway's user.

Example fix

// diagnosis: read the part before ';' as the real failure
// 'uplink connect failed: ...; additional gateway cleanup failure: permission denied removing ready file'
// after: fix permissions on daemon_root so cleanup succeeds
chmod u+rwX ~/.astrid/daemon
Defensive patterns

Strategy: try-catch

Try / catch

match run_gateway().await {
    Err(e) if e.to_string().contains("additional gateway cleanup failure") => {
        let (primary, _secondary) = e.to_string().split_once("; additional").unwrap();
        report_root_cause(primary);
    },
    other => other?,
}

Prevention

When it happens

Trigger: The accept loop exits with an error (Err(primary)) while gateway cleanup also returns Err(secondary); returned from run() and exercised by gateway_cleanup_failure_does_not_mask_accept_failure.

Common situations: A fatal accept-loop error (e.g. uplink/socket failure) coincides with cleanup failures like being unable to remove the gateway ready/lease files under daemon_root or socket cleanup failing due to permissions.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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