astrid-runtime/astrid · error

shutdown stage gateway.startup_reap: starting gateway PID {g

Error message

shutdown stage gateway.startup_reap: starting gateway PID {gateway_pid} is {outcome:?}

What it means

After sending SIGTERM/SIGKILL via daemon_control::terminate_known, the outcome was neither TermExited nor KilledExited — the starting gateway process did not confirm exit. The code reports the actual KillOutcome and bails.

Source

Thrown at crates/astrid-cli/src/commands/mcp/lifecycle.rs:618

    };
    let Some(gateway_exe) = lease.gateway_exe.as_deref() else {
        anyhow::bail!("shutdown stage gateway.startup_identity: lease has no executable");
    };
    let current = read_gateway_startup_lease()?.ok_or_else(|| {
        anyhow::anyhow!("shutdown stage gateway.startup_identity: lease disappeared")
    })?;
    if current != *lease {
        anyhow::bail!("shutdown stage gateway.startup_identity: startup generation changed");
    }

    let outcome =
        crate::commands::daemon_control::terminate_known(gateway_pid, Some(gateway_exe)).await;
    if !matches!(
        outcome,
        crate::commands::daemon_control::KillOutcome::TermExited
            | crate::commands::daemon_control::KillOutcome::KilledExited
    ) {
        anyhow::bail!(
            "shutdown stage gateway.startup_reap: starting gateway PID {gateway_pid} is {outcome:?}"
        );
    }
    Ok(())
}

async fn clean_unowned_gateway_startup(lifecycle: &GatewayLifecycleLock) -> Result<()> {
    let socket = gateway_socket_path()?;
    match astrid_core::local_transport::connect_outcome(&socket)
        .await
        .context("shutdown stage gateway.listener_probe")?
    {
        astrid_core::local_transport::ConnectOutcome::Absent => {},
        astrid_core::local_transport::ConnectOutcome::Stale => {
            astrid_core::local_transport::remove_stale_endpoint(&socket)
                .context("shutdown stage gateway.stale_listener_cleanup")?;
        },
        astrid_core::local_transport::ConnectOutcome::Connected(_) => anyhow::bail!(

View on GitHub (pinned to affd8760f4)

Solutions

  1. Check whether the PID is still alive and kill it manually, then remove gateway marker/lease files
  2. Re-run the shutdown after a short delay if the kill was racing process exit
  3. Run under the same user that started the gateway, or fix permissions in containers
  4. Inspect the reported outcome variant to determine the exact reap failure

Example fix

// before
stop_gateway().await?;
// after: handle non-exit reap outcomes
if let Err(e) = stop_gateway().await {
    if e.to_string().contains("gateway.startup_reap") {
        // remove stale markers manually once the process is confirmed dead
        remove_gateway_startup_lease(None)?;
    } else { return Err(e); }
}
Defensive patterns

Strategy: retry

Validate before calling

if !daemon_control::is_process_alive(gateway_pid) {
    // already gone; just clear markers instead of terminating
    remove_gateway_startup_lease(None)?;
    return Ok(());
}

Try / catch

match stop_gateway().await {
    Err(e) if e.to_string().contains("gateway.startup_reap") => {
        tokio::time::sleep(Duration::from_millis(500)).await;
        remove_gateway_startup_lease(None)?; // finish cleanup manually
    },
    other => other?,
}

Prevention

When it happens

Trigger: terminate_known returns outcomes like AlreadyDead mismatch, PermissionDenied, or the process survived the kill signal, when stop_gateway invokes stop_startup_gateway on the startup lease's PID.

Common situations: Gateway hung ignoring SIGTERM and SIGKILL delivery raced; PID reused by another process failing the exe identity check; insufficient permissions to signal the process (different user/container).

Related errors


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