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
- Let the running daemon finish: wait for the lock holder to exit, then re-run cleanup/stop.
- Confirm no live listener remains (check `astrid daemon status` / the socket) before forcing cleanup.
- Find the holder with `lsof <lock_path>` and kill the stale process if it's truly orphaned.
- 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
- Always stop the daemon through `astrid daemon stop` before any manual runtime cleanup.
- Use `astrid daemon status` (or probe the socket) as a preflight to cleanup.
- Monitor for orphaned daemon processes after crashes and reap them.
- Serialize daemon lifecycle operations per home in scripts/tests (don't run stop/start concurrently).
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
- failed to acquire daemon start fence: {error}
- shutdown stage daemon.singleton_lock: failed to acquire {}:
- {primary:#}; additional shutdown failure: {secondary:#}
- shutdown stage daemon.shutdown_ack: rejected: {reason}
- shutdown stage daemon.shutdown_ack: unexpected response: {ot
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/0c2738b0331b09f8.
Report an issue: GitHub.