astrid-runtime/astrid · warning
MCP attach replaced or idle-evicted
Error message
MCP attach replaced or idle-evicted
What it means
In `run_attached_session`, the attached session normally ends when the transport terminates (`running.waiting()`), but if the cancellation token fires first, this error is thrown. Cancellation means the session was replaced by a newer attach (same principal/peer key) or idle-evicted by the gateway, so the session was deliberately terminated rather than failing naturally.
Solutions
- Re-run the attach; if this session was intentionally replaced, the new session is already active and nothing else is needed.
- Avoid concurrent attaches with the same principal/peer key; detach the old session first.
- Keep the session active (or raise the idle-eviction policy) if idle timeouts are evicting it.
- Check gateway logs for who issued the cancellation (replacement vs. eviction) to confirm the cause.
Example fix
// before
let result = run_attached_session(...).await?; // hard-fails on replace/evict
// after
if let Err(e) = run_attached_session(...).await {
if e.to_string().contains("replaced or idle-evicted") {
eprintln!("session superseded; reconnecting...");
run_attached_session(...).await?;
} else { return Err(e); }
} Defensive patterns
Strategy: retry
Validate before calling
// Detect a live session with the same peer key before attaching
if gateway_has_active_session(&peer_key) {
eprintln!("session already attached; it will be replaced");
} Try / catch
match run_attached_session(...).await {
Err(e) if e.to_string().contains("replaced or idle-evicted") => {
eprintln!("superseded; reconnecting");
run_attached_session(...).await?;
}
other => other?,
} Prevention
- Do not attach the same principal/project from two clients simultaneously
- Keep sessions active or tune the idle-eviction policy
- Treat this error as an expected lifecycle event, not a crash
- Implement automatic reconnect with backoff in long-lived clients
When it happens
Trigger: A second `mcp attach` with the same peer key replaces the first session; the gateway's idle eviction policy cancels an inactive session; the gateway explicitly cancels the session's token during shutdown or lifecycle transitions.
Common situations: Opening the same MCP project in two editors/terminals so the newer attach supersedes the older one; leaving a session idle past the eviction timeout; the gateway restarting or being stopped while a session is attached.
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
- another MCP gateway startup or lifecycle is already active
- expected MCP attach registration
- MCP attach registration has an empty host
- MCP attach registration is missing or too large
- MCP gateway attach limit reached for principal
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/3523a8927ad3506c.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-cli/src/commands/mcp/gateway.rs:846
) -> Result<()>
where
S: rmcp::Service<RoleServer>,
R: tokio::io::AsyncRead + Unpin + Send + 'static,
{
let running = server
.serve_with_ct((reader, write_half), cancel.clone())
.await
.context("MCP gateway failed to initialize attach session")?;
// Use the slot generation rather than the host session id so a delayed
// predecessor cannot remove a replacement's peer from the shared watcher.
let peer_key = slot_id.to_string();
peers
.lock()
.await
.insert(peer_key.clone(), running.peer().clone());
let result = tokio::select! {
result = running.waiting() => result.context("MCP gateway attach transport terminated abnormally"),
() = cancel.cancelled() => Err(anyhow::anyhow!("MCP attach replaced or idle-evicted")),
};
peers.lock().await.remove(&peer_key);
result.map(|_| ())
}
async fn read_registration<R>(reader: &mut BufReader<R>) -> Result<AttachRegistration>
where
R: AsyncRead + Unpin,
{
timeout(REGISTRATION_TIMEOUT, read_registration_inner(reader))
.await
.context("timed out reading MCP attach registration")?
}
async fn read_registration_inner<R>(reader: &mut BufReader<R>) -> Result<AttachRegistration>
where
R: AsyncRead + Unpin,
{View on GitHub (pinned to affd8760f4)