{"record":{"id":"e21bb195713aef29","repo":"zeroclaw-labs/zeroclaw","slug":"local-ipc-endpoint-lifecycle-is-already-owned-at","errorCode":null,"errorMessage":"local IPC endpoint lifecycle is already owned at {}","messagePattern":"local IPC endpoint lifecycle is already owned at (.+?)","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/rpc/local.rs","lineNumber":406,"sourceCode":"                    // A swap between open and lock would let a second daemon\n                    // lock the replacement and re-enter the split-ownership\n                    // race this lock exists to prevent.\n                    let current = SocketIdentity::read(&lock_path).with_context(|| {\n                        format!(\n                            \"confirming local IPC endpoint lifecycle lock {}\",\n                            lock_path.display()\n                        )\n                    })?;\n                    if current != SocketIdentity::from_metadata(&metadata) {\n                        anyhow::bail!(\n                            \"local IPC endpoint lock {} was replaced while being \\\n                             acquired; refusing to share lifecycle ownership\",\n                            lock_path.display()\n                        );\n                    }\n                    Ok(Self { _file: file })\n                }\n                Err(TryLockError::WouldBlock) => Err(std::io::Error::new(\n                    ErrorKind::AddrInUse,\n                    format!(\n                        \"local IPC endpoint lifecycle is already owned at {}\",\n                        path.display()\n                    ),\n                )\n                .into()),\n                Err(TryLockError::Error(error)) => Err(error).with_context(|| {\n                    format!(\n                        \"locking local IPC endpoint lifecycle at {}\",\n                        lock_path.display()\n                    )\n                }),\n            }\n        }\n    }\n\n    /// Removes the bound socket only while the path still names this listener.","sourceCodeStart":388,"sourceCodeEnd":424,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/rpc/local.rs#L388-L424","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: blindly start a second daemon on the same socket\nlet (listener, guard) = rpc::local::bind(Path::new(\"/data/daemon.sock\")).await?; // AddrInUse: lifecycle already owned\n\n// after: detect the live owner first, and only bind when the endpoint is free\nlet path = Path::new(\"/data/daemon.sock\");\nif tokio::net::UnixStream::connect(path).await.is_ok() {\n    anyhow::bail!(\"a zeroclaw daemon is already serving at {}; reuse it or set ZEROCLAW_SOCKET\", path.display());\n}\nlet (listener, guard) = rpc::local::bind(path).await?;","handlingStrategy":"validation","validationCode":"use std::path::Path;\nuse tokio::net::UnixStream;\n\nasync fn endpoint_free(path: &Path) -> bool {\n    // A successful connect means a live daemon owns the endpoint.\n    UnixStream::connect(path).await.is_err()\n}\n\n// before spawning a daemon:\n// assert!(endpoint_free(Path::new(&socket_path)).await, \"daemon already running\");","typeGuard":"fn is_endpoint_in_use(err: &anyhow::Error) -> bool {\n    err.chain()\n        .filter_map(|c| c.downcast_ref::<std::io::Error>())\n        .any(|e| e.kind() == std::io::ErrorKind::AddrInUse)\n}","tryCatchPattern":"match rpc::local::bind(&path).await {\n    Ok(bound) => { /* serve */ }\n    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)) => {\n        eprintln!(\"another daemon already owns {}/.lock; stop it or set ZEROCLAW_SOCKET\", path.display());\n        std::process::exit(1);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["rust","unix-socket","daemon","file-lock","single-instance"],"backgroundTag":"address-in-use","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}