astrid-runtime/astrid · warning
MCP gateway readiness changed before cleanup at {}
Error message
MCP gateway readiness changed before cleanup at {} What it means
remove_gateway_ready_at performs a compare-and-delete: it re-reads the readiness file and only removes it if it still matches the record the caller captured earlier. If the file now holds a different record, a newer gateway generation has taken over and deleting it would break the successor, so this error is raised. If the file is already gone, cleanup succeeds silently.
Source
Thrown at crates/astrid-cli/src/commands/mcp/lifecycle.rs:350
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&temp, std::fs::Permissions::from_mode(0o600))?;
}
std::fs::rename(&temp, &path)
.with_context(|| format!("failed to publish {}", path.display()))?;
Ok(())
}
/// Remove this gateway's readiness marker without deleting a successor's.
pub(crate) fn remove_gateway_ready(record: &GatewayReady) -> Result<()> {
let path = gateway_ready_path()?;
remove_gateway_ready_at(&path, record)
}
fn remove_gateway_ready_at(path: &Path, record: &GatewayReady) -> Result<()> {
match read_gateway_ready_at(path)? {
Some(current) if current == *record => std::fs::remove_file(path)
.with_context(|| format!("failed to remove {}", path.display())),
Some(_) => anyhow::bail!(
"MCP gateway readiness changed before cleanup at {}",
path.display()
),
None => Ok(()),
}
}
/// Create a private runtime directory, preserving the Astrid home boundary.
pub(crate) fn ensure_private_dir(path: &Path) -> Result<()> {
std::fs::create_dir_all(path).with_context(|| {
format!(
"failed to create private runtime directory {}",
path.display()
)
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;View on GitHub (pinned to affd8760f4)
Solutions
- Treat as success-for-successor: skip removal since the new generation owns the record
- Serialize gateway start/stop so an old instance can't clean up after a new one starts
- Re-read the current record before cleanup and only remove if you own it
- Use per-generation unique readiness paths if concurrency is expected
Example fix
// before
remove_gateway_ready_at(&path, &old_record)?;
// after
if let Ok(Some(current)) = read_gateway_ready_at(&path) {
if current == old_record { remove_gateway_ready_at(&path, &old_record)?; }
} Defensive patterns
Strategy: try-catch
Validate before calling
let current = read_gateway_ready_at(&path)?;
if current.as_ref() != Some(record) { /* successor owns the record — skip removal */ } Try / catch
match remove_gateway_ready_at(&path, &record) {
Ok(()) => {},
Err(e) if e.to_string().contains("readiness changed") => {}, // successor gateway owns it; ignore
Err(e) => return Err(e),
} Prevention
- Capture the readiness record only from your own gateway startup
- Serialize gateway restarts so old instances finish cleanup first
- Use unique readiness paths per generation when overlap is possible
When it happens
Trigger: remove_gateway_ready (or test ready_cleanup_cannot_remove_a_successor_record) attempts cleanup with an old GatewayReady record while the file has been overwritten by a newer gateway with different pid/principal/token.
Common situations: Gateway restarted between capturing the record and cleanup; overlapping test runs sharing a runtime dir; delayed shutdown of an old instance after a successor started.
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
- MCP gateway startup generation changed before cleanup
- MCP gateway is shutting down
- MCP gateway attach limit reached for principal '{principal}'
- invalid MCP gateway startup lease at {}
- invalid MCP gateway readiness metadata at {}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/17b6f20be8b1b345.
Report an issue: GitHub.