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.
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
- invalid MCP gateway readiness metadata at
- invalid MCP gateway startup lease at
- MCP gateway attach limit reached for principal
- MCP gateway is shutting down
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/17b6f20be8b1b345.
Report an issue: GitHub.
Appendix: 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)