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

  1. Treat as success-for-successor: skip removal since the new generation owns the record
  2. Serialize gateway start/stop so an old instance can't clean up after a new one starts
  3. Re-read the current record before cleanup and only remove if you own it
  4. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/17b6f20be8b1b345. Report an issue: GitHub.