astrid-runtime/astrid · error
shutdown stage gateway.startup_stop: lifecycle changed
Error message
shutdown stage gateway.startup_stop: lifecycle changed
What it means
During `stop_gateway`, if the shutdown discovers the gateway is still in its startup phase, it waits for readiness and then re-acquires the lifecycle lock to clean up the startup artifacts. This error is thrown when that re-acquisition fails (returns `None`), meaning the lock was taken by someone else between the check and the reacquire — the lifecycle changed mid-shutdown (e.g. a new gateway started, or another stop won the race).
Solutions
- Re-run the stop command; the racing operation has completed and the current state is likely what you want.
- Check whether a new gateway instance started during shutdown (check socket/ready file) and decide whether to stop it again.
- Avoid running concurrent start/stop operations; serialize lifecycle commands in scripts.
- Disable or pause auto-restart supervisors (systemd unit, daemon watchdog) while manually stopping.
Example fix
// before astrid mcp gateway stop # may race with restart // after systemctl --user stop astrid-gateway # pause supervisor first astrid mcp gateway stop
Defensive patterns
Strategy: retry
Validate before calling
// Before stopping, check no start is in progress
if startup_lease_exists() {
eprintln!("gateway is starting; wait for readiness before stopping");
} Try / catch
if let Err(e) = stop_gateway().await {
if e.to_string().contains("lifecycle changed") {
eprintln!("lifecycle raced; retrying stop");
stop_gateway().await?;
} else { return Err(e); }
} Prevention
- Serialize gateway start/stop commands in scripts
- Pause auto-restart supervisors before manual stops
- Wait for readiness before issuing stop during startup
- Run only one lifecycle command at a time
When it happens
Trigger: Concurrent `gateway run` (or a supervisor restart) grabbing the lifecycle lock exactly while `stop_gateway` is waiting through the ready-poll loop; two concurrent `stop_gateway` invocations racing.
Common situations: A daemon supervisor auto-restarting the gateway during a manual stop; scripts running start and stop simultaneously; a user starting the gateway in one terminal while another stops it.
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
- shutdown stage gateway.startup_identity: lease disappeared
- another MCP gateway startup or lifecycle is already active
- invalid MCP gateway readiness metadata at
- invalid MCP gateway startup lease at
- MCP gateway lock path has no parent
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8b09c1b02d680859.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-cli/src/commands/mcp/lifecycle.rs:547
}
if let Some(lease) = read_gateway_startup_lease()?
&& lease
.gateway_pid
.is_some_and(crate::commands::daemon_control::is_process_alive)
{
stop_startup_gateway(&lease).await?;
continue;
}
if Instant::now() >= deadline {
anyhow::bail!(
"shutdown stage gateway.startup_stop: a starting gateway did not become stoppable within {} seconds",
READY_TIMEOUT.as_secs()
);
}
tokio::time::sleep(READY_POLL).await;
}
let lifecycle = try_acquire_gateway_lifecycle()?.ok_or_else(|| {
anyhow::anyhow!("shutdown stage gateway.startup_stop: lifecycle changed")
})?;
clean_unowned_gateway_startup(&lifecycle)
.await
.context("shutdown stage gateway.stale_listener_cleanup")?;
drop(lifecycle);
return Ok(());
};
stop_ready_gateway(record, socket).await
}
async fn stop_ready_gateway(record: GatewayReady, socket: PathBuf) -> Result<()> {
let stream = match astrid_core::local_transport::connect_outcome(&socket)
.await
.context("shutdown stage gateway.listener_probe")?
{
astrid_core::local_transport::ConnectOutcome::Connected(stream) => stream,
astrid_core::local_transport::ConnectOutcome::Absent
| astrid_core::local_transport::ConnectOutcome::StaleView on GitHub (pinned to affd8760f4)