{"record":{"id":"a601aaffe9f65032","repo":"astrid-runtime/astrid","slug":"shutdown-stage-gateway-lifecycle-fence-a-successo","errorCode":null,"errorMessage":"shutdown stage gateway.lifecycle_fence: a successor gateway lifecycle remains active","messagePattern":"shutdown stage gateway\\.lifecycle_fence: a successor gateway lifecycle remains active","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/astrid-cli/src/commands/mcp/lifecycle.rs","lineNumber":717,"sourceCode":"            .context(\"failed to read control frame\")?;\n        if byte == b'\\n' {\n            return Ok(line);\n        }\n        line.push(byte);\n    }\n    anyhow::bail!(\"MCP gateway control frame is missing or too large\")\n}\n\nasync fn remove_dead_gateway_markers(record: &GatewayReady) -> Result<()> {\n    if crate::commands::daemon_control::is_process_alive(record.pid) {\n        anyhow::bail!(\n            \"shutdown stage gateway.process_reap: PID {} is still alive\",\n            record.pid\n        );\n    }\n    let lifecycle = try_acquire_gateway_lifecycle()?;\n    let Some(lifecycle) = lifecycle else {\n        anyhow::bail!(\n            \"shutdown stage gateway.lifecycle_fence: a successor gateway lifecycle remains active\"\n        );\n    };\n    let socket = gateway_socket_path()?;\n    match astrid_core::local_transport::connect_outcome(&socket)\n        .await\n        .context(\"shutdown stage gateway.listener_probe\")?\n    {\n        astrid_core::local_transport::ConnectOutcome::Connected(_) => {\n            anyhow::bail!(\n                \"shutdown stage gateway.listener_absence: a gateway is still accepting connections\"\n            );\n        },\n        astrid_core::local_transport::ConnectOutcome::Absent => {},\n        astrid_core::local_transport::ConnectOutcome::Stale => {\n            astrid_core::local_transport::remove_stale_endpoint(&socket)\n                .context(\"shutdown stage gateway.listener_cleanup\")?;\n        },","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/mcp/lifecycle.rs#L699-L735","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"// before\nremove_dead_gateway_markers(&record).await?;\n// after: treat fenced cleanup as benign\nmatch remove_dead_gateway_markers(&record).await {\n    Err(e) if e.to_string().contains(\"lifecycle_fence\") => {\n        // successor lifecycle active; nothing to clean\n    },\n    other => other?,\n}","handlingStrategy":"fallback","validationCode":"if try_acquire_gateway_lifecycle()?.is_none() {\n    // successor lifecycle active; skip cleanup entirely\n    return Ok(());\n}","typeGuard":null,"tryCatchPattern":"match remove_dead_gateway_markers(&record).await {\n    Err(e) if e.to_string().contains(\"lifecycle_fence\") => {\n        // benign: another generation owns cleanup\n    },\n    other => other?,\n}","preventionTips":["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"],"tags":["concurrency","locking","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}