astrid-runtime/astrid · warning
shutdown stage gateway.lifecycle_fence: a successor gateway…
Error message
shutdown stage gateway.lifecycle_fence: a successor gateway lifecycle remains active
What it means
After confirming the gateway process is dead, remove_dead_gateway_markers tries to acquire the gateway lifecycle fence; if another process already holds it (try_acquire_gateway_lifecycle returns None), a successor gateway lifecycle is active and this cleanup must not proceed.
Solutions
- Do nothing: the successor lifecycle owns cleanup — treat this as benign and skip
- Retry later once the other client finishes its startup/shutdown
- Audit for multiple concurrent astrid clients sharing one state directory and serialize them
- If the lock holder is a dead process, clear the lock/state files manually and retry
Example fix
// before
remove_dead_gateway_markers(&record).await?;
// after: treat fenced cleanup as benign
match remove_dead_gateway_markers(&record).await {
Err(e) if e.to_string().contains("lifecycle_fence") => {
// successor lifecycle active; nothing to clean
},
other => other?,
} Defensive patterns
Strategy: fallback
Validate before calling
if try_acquire_gateway_lifecycle()?.is_none() {
// successor lifecycle active; skip cleanup entirely
return Ok(());
} Try / catch
match remove_dead_gateway_markers(&record).await {
Err(e) if e.to_string().contains("lifecycle_fence") => {
// benign: another generation owns cleanup
},
other => other?,
} Prevention
- Avoid multiple concurrent astrid clients sharing one state directory
- Let the lock holder finish before running cleanup
- Treat fenced cleanup as expected during overlapping start/stop
When it happens
Trigger: Two concurrent shutdown/startup sequences: another astrid client holds the gateway lifecycle lock while this one attempts marker cleanup via wait_for_gateway or stop_ready_gateway.
Common situations: Multiple terminals/IDE windows each running an astrid MCP client; a startup raced this shutdown and legitimately fenced it out; a crashed process left the lock held (depending on lock implementation).
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
- Astrid volume is already open
- MCP gateway readiness changed before cleanup at
- MCP gateway startup generation changed before cleanup
- AlreadyExists
- AlreadyExists
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/a601aaffe9f65032.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-cli/src/commands/mcp/lifecycle.rs:717
.context("failed to read control frame")?;
if byte == b'\n' {
return Ok(line);
}
line.push(byte);
}
anyhow::bail!("MCP gateway control frame is missing or too large")
}
async fn remove_dead_gateway_markers(record: &GatewayReady) -> Result<()> {
if crate::commands::daemon_control::is_process_alive(record.pid) {
anyhow::bail!(
"shutdown stage gateway.process_reap: PID {} is still alive",
record.pid
);
}
let lifecycle = try_acquire_gateway_lifecycle()?;
let Some(lifecycle) = lifecycle else {
anyhow::bail!(
"shutdown stage gateway.lifecycle_fence: a successor gateway lifecycle remains active"
);
};
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::Connected(_) => {
anyhow::bail!(
"shutdown stage gateway.listener_absence: a gateway is still accepting connections"
);
},
astrid_core::local_transport::ConnectOutcome::Absent => {},
astrid_core::local_transport::ConnectOutcome::Stale => {
astrid_core::local_transport::remove_stale_endpoint(&socket)
.context("shutdown stage gateway.listener_cleanup")?;
},View on GitHub (pinned to affd8760f4)