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
- Check whether the PID is still alive and kill it manually, then remove gateway marker/lease files
- Re-run the shutdown after a short delay if the kill was racing process exit
- Run under the same user that started the gateway, or fix permissions in containers
- 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
- Run stop under the same user that started the gateway
- Avoid killing the gateway externally before calling stop
- Allow a grace period between failed kills and retries
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
- shutdown stage daemon.process_reap: daemon did not exit afte
- shutdown stage gateway.startup_identity: lease has no gatewa
- shutdown stage gateway.startup_identity: lease has no execut
- {primary:#}; additional shutdown failure: {secondary:#}
- shutdown stage daemon.shutdown_ack: rejected: {reason}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/376a1f36b311de50.
Report an issue: GitHub.