astrid-runtime/astrid · error
shutdown stage gateway.startup_identity: startup generation
Error message
shutdown stage gateway.startup_identity: startup generation changed
What it means
Thrown when the lease currently on disk differs from the in-memory lease being shut down, meaning a newer gateway startup generation has replaced this one. Stopping the old lease's process would be wrong, so the code bails.
Source
Thrown at crates/astrid-cli/src/commands/mcp/lifecycle.rs:608
"shutdown stage gateway.process_reap: authenticated gateway PID {} did not exit",
record.pid
);
}
remove_dead_gateway_markers(&record).await
}
async fn stop_startup_gateway(lease: &GatewayStartupLease) -> Result<()> {
let Some(gateway_pid) = lease.gateway_pid else {
anyhow::bail!("shutdown stage gateway.startup_identity: lease has no gateway PID");
};
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()?;View on GitHub (pinned to affd8760f4)
Solutions
- Re-read the current lease and only stop the gateway you actually own (retry the stop against the newest lease)
- Serialize startup/shutdown with a lock so only one generation exists at a time
- If the old gateway is harmless, skip the stop — its markers are now owned by the new generation
- Check for duplicate cron/systemd units or IDE extensions each starting the gateway
Example fix
// before
stop_gateway().await?;
// after: tolerate generation change
match stop_gateway().await {
Err(e) if e.to_string().contains("startup generation changed") => {
// newer gateway owns the lease; nothing to do
},
other => other?,
} Defensive patterns
Strategy: retry
Validate before calling
let on_disk = read_gateway_startup_lease()?;
if on_disk.as_ref() != Some(&my_lease) {
// generation already replaced; skip the stop
return Ok(());
} Type guard
fn lease_is_current(mine: &GatewayStartupLease) -> bool {
read_gateway_startup_lease()
.ok()
.flatten()
.map(|cur| cur == *mine)
.unwrap_or(false)
} Try / catch
match stop_gateway().await {
Err(e) if e.to_string().contains("startup generation changed") => {
// benign: a newer generation owns the lease
},
other => other?,
} Prevention
- Serialize gateway start/stop behind a single lock or supervisor
- Avoid running multiple astrid clients against one state directory
- Re-read the lease immediately before stopping
When it happens
Trigger: Two overlapping gateway startups: process A holds a lease, process B starts and overwrites the lease, then A's stop_gateway runs and detects current != *lease. Also happens when stop is invoked after a concurrent re-start.
Common situations: Running two astrid MCP clients simultaneously; a watchdog/auto-restart spawned a new gateway while an old shutdown was in flight; stale script calling stop twice across generations.
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
- capsule {} disappeared during durable contracts scan
- corpus input changed while its baseline snapshot was capture
- mountpoint was concurrently registered: {}
- workspace capsule manifest changed while it was being read:
- durable capsule package '{id}' disappeared during removal
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/715904771b1d1cad.
Report an issue: GitHub.