astrid-runtime/astrid · error

daemon closed guard uplink

Error message

daemon closed guard uplink

What it means

hold_guard_uplink keeps the MCP session guard's uplink alive by reading raw frames from the daemon client in a loop. When read_raw_frame returns Ok(None) the daemon has closed the connection (EOF), and the guard bails with this error because an active guard cannot persist without the daemon uplink. It exists to surface daemon-side disconnects rather than silently ending the guard session.

Source

Thrown at crates/astrid-cli/src/commands/mcp/session_guard.rs:54

    principal: &astrid_core::PrincipalId,
    daemon_root: &Path,
) -> Result<std::convert::Infallible> {
    daemon::ensure_daemon_quiet("mcp-session-guard", Some(daemon_root)).await?;

    let session = astrid_core::SessionId::from_uuid(Uuid::new_v4());
    let c =
        crate::socket_client::connect_for_workspace(session, principal.clone(), Some(daemon_root))
            .await
            .context("failed to connect guard uplink to daemon")?;

    validate_guard_auth(principal, c.is_authenticated())?;

    debug!(%principal, "MCP session guard: daemon uplink established");
    let mut client = c;
    loop {
        match client.read_raw_frame().await {
            Ok(Some(_)) => {},
            Ok(None) => anyhow::bail!("daemon closed guard uplink"),
            Err(e) => return Err(e).context("guard uplink read failed"),
        }
    }
}

fn validate_guard_auth(principal: &astrid_core::PrincipalId, authenticated: bool) -> Result<()> {
    if authenticated || *principal == astrid_core::PrincipalId::anonymous() {
        return Ok(());
    }

    anyhow::bail!(
        "guard uplink authenticated as anonymous instead of requested principal '{principal}'"
    )
}

#[cfg(test)]
mod tests {
    use super::*;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Check daemon logs for why the guard uplink was closed (shutdown, revocation, crash) at the matching time.
  2. Restart the daemon and re-establish the session guard.
  3. Make the guard resilient: on Ok(None), reconnect to the daemon and re-run the guard handshake instead of bailing.
  4. Run the daemon under a supervisor (systemd) so it restarts and the guard can re-acquire the uplink.

Example fix

// before
Ok(None) => anyhow::bail!("daemon closed guard uplink"),
// after
Ok(None) => {
    // reconnect with backoff instead of failing the guard session
    client = reconnect_guard_uplink(principal).await?;
    continue;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify daemon liveness before holding the guard uplink
if !is_daemon_alive().await {
    anyhow::bail!("daemon not running; guard uplink will close immediately");
}

Try / catch

match hold_guard_uplink(client, principal).await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("daemon closed guard uplink") => {
        // daemon went away: restart/reconnect and re-establish the guard
        restart_daemon_and_reacquire_guard(principal).await?;
    },
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: The daemon process shuts down or crashes while the session guard is holding the uplink; the daemon explicitly closes the guard connection (e.g. session revoked, shutdown of the guard service); network/socket drop producing a clean EOF (Ok(None)) rather than an Err.

Common situations: Daemon restarted for an upgrade while a guard was active; system shutdown or OOM kill of the daemon; daemon closing the guard because the session expired or was revoked; socket cleanup by an init/supervisor.

Related errors


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