astrid-runtime/astrid · error

shutdown stage gateway.startup_stop: lifecycle changed

Error message

shutdown stage gateway.startup_stop: lifecycle changed

What it means

During `stop_gateway`, if the shutdown discovers the gateway is still in its startup phase, it waits for readiness and then re-acquires the lifecycle lock to clean up the startup artifacts. This error is thrown when that re-acquisition fails (returns `None`), meaning the lock was taken by someone else between the check and the reacquire — the lifecycle changed mid-shutdown (e.g. a new gateway started, or another stop won the race).

Solutions

  1. Re-run the stop command; the racing operation has completed and the current state is likely what you want.
  2. Check whether a new gateway instance started during shutdown (check socket/ready file) and decide whether to stop it again.
  3. Avoid running concurrent start/stop operations; serialize lifecycle commands in scripts.
  4. Disable or pause auto-restart supervisors (systemd unit, daemon watchdog) while manually stopping.

Example fix

// before
astrid mcp gateway stop   # may race with restart
// after
systemctl --user stop astrid-gateway   # pause supervisor first
astrid mcp gateway stop
Defensive patterns

Strategy: retry

Validate before calling

// Before stopping, check no start is in progress
if startup_lease_exists() {
    eprintln!("gateway is starting; wait for readiness before stopping");
}

Try / catch

if let Err(e) = stop_gateway().await {
    if e.to_string().contains("lifecycle changed") {
        eprintln!("lifecycle raced; retrying stop");
        stop_gateway().await?;
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: Concurrent `gateway run` (or a supervisor restart) grabbing the lifecycle lock exactly while `stop_gateway` is waiting through the ready-poll loop; two concurrent `stop_gateway` invocations racing.

Common situations: A daemon supervisor auto-restarting the gateway during a manual stop; scripts running start and stop simultaneously; a user starting the gateway in one terminal while another stops it.

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/8b09c1b02d680859. Report an issue: GitHub.

Appendix: source

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

            }
            if let Some(lease) = read_gateway_startup_lease()?
                && lease
                    .gateway_pid
                    .is_some_and(crate::commands::daemon_control::is_process_alive)
            {
                stop_startup_gateway(&lease).await?;
                continue;
            }
            if Instant::now() >= deadline {
                anyhow::bail!(
                    "shutdown stage gateway.startup_stop: a starting gateway did not become stoppable within {} seconds",
                    READY_TIMEOUT.as_secs()
                );
            }
            tokio::time::sleep(READY_POLL).await;
        }
        let lifecycle = try_acquire_gateway_lifecycle()?.ok_or_else(|| {
            anyhow::anyhow!("shutdown stage gateway.startup_stop: lifecycle changed")
        })?;
        clean_unowned_gateway_startup(&lifecycle)
            .await
            .context("shutdown stage gateway.stale_listener_cleanup")?;
        drop(lifecycle);
        return Ok(());
    };
    stop_ready_gateway(record, socket).await
}

async fn stop_ready_gateway(record: GatewayReady, socket: PathBuf) -> Result<()> {
    let stream = match astrid_core::local_transport::connect_outcome(&socket)
        .await
        .context("shutdown stage gateway.listener_probe")?
    {
        astrid_core::local_transport::ConnectOutcome::Connected(stream) => stream,
        astrid_core::local_transport::ConnectOutcome::Absent
        | astrid_core::local_transport::ConnectOutcome::Stale

View on GitHub (pinned to affd8760f4)