astrid-runtime/astrid · error

shutdown stage daemon.singleton_lock: lock remains held at {

Error message

shutdown stage daemon.singleton_lock: lock remains held at {}

What it means

A shutdown-stage error emitted by `cleanup_daemon_runtime_for_home` when `try_lock()` on the daemon singleton lock file returns `TryLockError::WouldBlock`, meaning another process still holds the lock. Cleanup refuses to proceed so it cannot delete runtime state (markers, socket) out from under a live daemon. The message includes the lock path for diagnosis.

Source

Thrown at crates/astrid-cli/src/commands/daemon.rs:894

    if !runtime_present {
        return Ok(());
    }
    let lock_path = home.run_dir().join("system.lock");
    let mut options = std::fs::OpenOptions::new();
    options.read(true).write(true).create(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;
        options.mode(0o600);
    }
    let lock = options.open(&lock_path).with_context(|| {
        format!(
            "shutdown stage daemon.singleton_lock: open {}",
            lock_path.display()
        )
    })?;
    lock.try_lock().map_err(|error| match error {
        std::fs::TryLockError::WouldBlock => anyhow::anyhow!(
            "shutdown stage daemon.singleton_lock: lock remains held at {}",
            lock_path.display()
        ),
        std::fs::TryLockError::Error(error) => anyhow::anyhow!(
            "shutdown stage daemon.singleton_lock: failed to acquire {}: {error}",
            lock_path.display()
        ),
    })?;

    match astrid_core::local_transport::connect_outcome(socket_path)
        .await
        .context("shutdown stage daemon.listener_probe")?
    {
        astrid_core::local_transport::ConnectOutcome::Connected(_) => {
            anyhow::bail!(
                "shutdown stage daemon.listener_absence: endpoint remains live at {}",
                socket_path.display()
            );

View on GitHub (pinned to affd8760f4)

Solutions

  1. Let the running daemon finish: wait for the lock holder to exit, then re-run cleanup/stop.
  2. Confirm no live listener remains (check `astrid daemon status` / the socket) before forcing cleanup.
  3. Find the holder with `lsof <lock_path>` and kill the stale process if it's truly orphaned.
  4. Re-run the stop command; cleanup is idempotent once the lock is free.
Defensive patterns

Strategy: validation

Validate before calling

// preflight: is a daemon still alive for this home?
if astrid_cli::local_transport::connect_outcome(&socket_path).is_ok() {
    return Err(anyhow!("daemon still live at {socket_path}; stop it before cleanup"));
}

Try / catch

match cleanup_daemon_runtime() {
    Err(e) if e.to_string().contains("lock remains held") => {
        eprintln!("another daemon still holds the singleton lock; waiting...");
        wait_for_lock_release(lock_path, timeout)?;
        cleanup_daemon_runtime()
    }
    other => other,
}

Prevention

When it happens

Trigger: Running `astrid daemon stop`/cleanup for a home whose singleton lock is currently held by a running daemon or another CLI process; calling cleanup while a live listener is still serving on the socket.

Common situations: Attempted cleanup while the daemon is genuinely still running (stop didn't complete); an orphaned process holds the lock after a crash; tests concurrently starting/stopping daemons for the same home.

Related errors


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