astrid-runtime/astrid · error

shutdown stage gateway.startup_identity: lease disappeared

Error message

shutdown stage gateway.startup_identity: lease disappeared

What it means

`stop_startup_gateway` shuts down a gateway that is still starting. It reads the on-disk startup lease a second time and compares it to the lease observed earlier; this error is thrown when the lease file has disappeared, meaning the startup generation ended (crashed, completed and replaced by the ready record, or cleaned up by another process) between the two reads.

Solutions

  1. Re-run the stop command; the transient startup generation has ended, so re-evaluating state will follow the normal stop path.
  2. Check the ready file/socket: if the gateway finished starting, it will be stopped via the normal (non-startup) path on retry.
  3. Avoid racing multiple stop commands; run one shutdown at a time.
  4. Inspect gateway logs to confirm the starting process crashed or completed during shutdown.

Example fix

// before
astrid mcp gateway stop   # single attempt may hit transient lease removal
// after
astrid mcp gateway stop || astrid mcp gateway stop
Defensive patterns

Strategy: retry

Validate before calling

// Check current startup state before stopping
let leasing = read_gateway_startup_lease()?;
eprintln!("startup lease present: {}", leasing.is_some());

Try / catch

if let Err(e) = stop_gateway().await {
    if e.to_string().contains("lease disappeared") {
        eprintln!("startup generation ended mid-stop; retrying");
        stop_gateway().await?;
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: The startup lease file (`mcp-gateway.starting`) is deleted between `stop_gateway` reading it and `stop_startup_gateway` re-reading it — e.g. the starting gateway crashed or finished starting (lease removed/converted to ready) concurrently, or a stale-startup cleanup ran in between.

Common situations: Stopping the gateway at the exact moment its startup completes or fails; a concurrent `stop` or cleanup process removing the lease; the starting gateway process dying mid-startup.

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/5ff0bb7a25764d98. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/mcp/lifecycle.rs:605

    .await
    {
        anyhow::bail!(
            "shutdown stage gateway.process_reap: authenticated gateway PID {} did not exit",
            record.pid
        );
    }
    remove_dead_gateway_markers(&record).await
}

async fn stop_startup_gateway(lease: &GatewayStartupLease) -> Result<()> {
    let Some(gateway_pid) = lease.gateway_pid else {
        anyhow::bail!("shutdown stage gateway.startup_identity: lease has no gateway PID");
    };
    let Some(gateway_exe) = lease.gateway_exe.as_deref() else {
        anyhow::bail!("shutdown stage gateway.startup_identity: lease has no executable");
    };
    let current = read_gateway_startup_lease()?.ok_or_else(|| {
        anyhow::anyhow!("shutdown stage gateway.startup_identity: lease disappeared")
    })?;
    if current != *lease {
        anyhow::bail!("shutdown stage gateway.startup_identity: startup generation changed");
    }

    let outcome =
        crate::commands::daemon_control::terminate_known(gateway_pid, Some(gateway_exe)).await;
    if !matches!(
        outcome,
        crate::commands::daemon_control::KillOutcome::TermExited
            | crate::commands::daemon_control::KillOutcome::KilledExited
    ) {
        anyhow::bail!(
            "shutdown stage gateway.startup_reap: starting gateway PID {gateway_pid} is {outcome:?}"
        );
    }
    Ok(())
}

View on GitHub (pinned to affd8760f4)