zeroclaw-labs/zeroclaw · error · std::io::Error
local IPC endpoint lifecycle is already owned at {}
Error message
local IPC endpoint lifecycle is already owned at {} What it means
Raised by EndpointLock::acquire (crates/zeroclaw-runtime/src/rpc/local.rs:406) when taking an advisory flock() on the companion '<socket-path>.lock' file returns WouldBlock. The lock serializes ownership of a Unix-socket IPC endpoint's lifecycle, so this error means another live process (typically a second zeroclaw daemon) currently holds the endpoint lock for the same socket path. Because flock is released automatically when the holder exits, a WouldBlock almost always indicates a genuinely running daemon, not a stale leftover.
Source
Thrown at crates/zeroclaw-runtime/src/rpc/local.rs:406
// A swap between open and lock would let a second daemon
// lock the replacement and re-enter the split-ownership
// race this lock exists to prevent.
let current = SocketIdentity::read(&lock_path).with_context(|| {
format!(
"confirming local IPC endpoint lifecycle lock {}",
lock_path.display()
)
})?;
if current != SocketIdentity::from_metadata(&metadata) {
anyhow::bail!(
"local IPC endpoint lock {} was replaced while being \
acquired; refusing to share lifecycle ownership",
lock_path.display()
);
}
Ok(Self { _file: file })
}
Err(TryLockError::WouldBlock) => Err(std::io::Error::new(
ErrorKind::AddrInUse,
format!(
"local IPC endpoint lifecycle is already owned at {}",
path.display()
),
)
.into()),
Err(TryLockError::Error(error)) => Err(error).with_context(|| {
format!(
"locking local IPC endpoint lifecycle at {}",
lock_path.display()
)
}),
}
}
}
/// Removes the bound socket only while the path still names this listener.View on GitHub (pinned to 88bb9c8533)
Solutions
- Check whether a daemon is already serving: connect to the socket (`zeroclaw status`, or `socat - UNIX-CONNECT:<path>` / a curl to the gateway) and reuse that instance instead of starting another.
- If you truly want a second instance, give it its own data dir or set ZEROCLAW_SOCKET to a distinct path so each daemon owns a different endpoint lock.
- Find the lock holder and stop it: `fuser <path>.lock` or `lsof <path>.lock`, then stop that process cleanly (systemctl stop zeroclaw, or kill the PID). Do not just delete the .lock file — flock state lives on the open file description, not the directory entry, and deleting it can create the split-ownership race the lock exists to prevent.
- If a supervisor restarts the daemon, add a pre-start check that fails when the socket answers, so restarts wait for the old process to release the lock.
Example fix
// before: blindly start a second daemon on the same socket
let (listener, guard) = rpc::local::bind(Path::new("/data/daemon.sock")).await?; // AddrInUse: lifecycle already owned
// after: detect the live owner first, and only bind when the endpoint is free
let path = Path::new("/data/daemon.sock");
if tokio::net::UnixStream::connect(path).await.is_ok() {
anyhow::bail!("a zeroclaw daemon is already serving at {}; reuse it or set ZEROCLAW_SOCKET", path.display());
}
let (listener, guard) = rpc::local::bind(path).await?; Defensive patterns
Strategy: validation
Validate before calling
use std::path::Path;
use tokio::net::UnixStream;
async fn endpoint_free(path: &Path) -> bool {
// A successful connect means a live daemon owns the endpoint.
UnixStream::connect(path).await.is_err()
}
// before spawning a daemon:
// assert!(endpoint_free(Path::new(&socket_path)).await, "daemon already running"); Type guard
fn is_endpoint_in_use(err: &anyhow::Error) -> bool {
err.chain()
.filter_map(|c| c.downcast_ref::<std::io::Error>())
.any(|e| e.kind() == std::io::ErrorKind::AddrInUse)
} Try / catch
match rpc::local::bind(&path).await {
Ok(bound) => { /* serve */ }
Err(e) if e.chain().any(|c| matches!(c.downcast_ref::<std::io::Error>(), Some(io_err) if io_err.kind() == std::io::ErrorKind::AddrInUse)) => {
eprintln!("another daemon already owns {}/.lock; stop it or set ZEROCLAW_SOCKET", path.display());
std::process::exit(1);
}
Err(e) => return Err(e),
} Prevention
- Run the daemon under a single-instance supervisor (systemd) so restarts never overlap the old holder.
- Give each daemon instance its own data dir / ZEROCLAW_SOCKET; never share an endpoint between deployments.
- Add a pre-start health check that connects to the socket and refuses to spawn a second daemon when it answers.
- Never 'fix' this by deleting the .lock file — the flock lives on the open descriptor; deleting the file invites the split-ownership race the lock prevents.
When it happens
Trigger: Calling bind(path) / EndpointLock::acquire(path) while another process holds the flock on path.with_extension(".lock") (e.g. '<data-dir>/daemon.sock.lock'). Concretely: starting `zeroclaw daemon` a second time with the same data dir or the same ZEROCLAW_SOCKET while the first daemon is still alive (systemd service plus a manual foreground run is the classic case).
Common situations: Running the daemon manually while a systemd/user service instance is already active; two agents pointed at the same data dir; CI or dev shells reusing a shared ZEROCLAW_SOCKET; a supervisor that spawns a new daemon before the old process fully exits; running the daemon twice in two terminals by accident.
Related errors
- local IPC endpoint is already serving at {}
- local IPC endpoint changed while being probed at {}
- Timed out waiting for auth profile lock at {}
- purge_agent not supported by this memory backend
- rename_agent not supported by this memory backend
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e21bb195713aef29.
Report an issue: GitHub.