{"record":{"id":"fad22d73976bf547","repo":"zeroclaw-labs/zeroclaw","slug":"local-ipc-endpoint-lock-was-replaced-while-bein","errorCode":null,"errorMessage":"local IPC endpoint lock {} was replaced while being acquired; refusing to share lifecycle ownership","messagePattern":"local IPC endpoint lock (.+?) was replaced while being acquired; refusing to share lifecycle ownership","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/rpc/local.rs","lineNumber":398,"sourceCode":"                    lock_path.display()\n                )\n            })?;\n            require_trusted_lock_file(&metadata, &lock_path)?;\n\n            match file.try_lock() {\n                Ok(()) => {\n                    // The directory entry must still name the locked inode.\n                    // 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 {}\",","sourceCodeStart":380,"sourceCodeEnd":416,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/rpc/local.rs#L380-L416","documentation":"The dev/inode identity of `<socket>.lock` changed between opening the file and taking the flock: something replaced the directory entry mid-acquisition. Accepting the replacement would let a second daemon lock a different inode and both believe they own socket lifecycle cleanup, so ZeroClaw bails instead of sharing ownership.","triggerScenarios":"A cleanup process unlinks and recreates the lock exactly while the daemon starts; two daemons racing to start with one creating a fresh lock over the other's; configuration management atomically replacing files in the runtime dir (mv into place).","commonSituations":"Restart loops where a stopping daemon's cleanup recreates state while the next one starts; scripts that 'reset' the socket directory by rm+mkdir on every service start; hostile co-tenant in a shared directory swapping the lock.","solutions":["Retry acquisition once — a fresh, stable lock usually resolves it.","Stop whatever replaces the lock: remove rm/recreate logic for the socket dir from unit files and scripts (create-if-missing only).","Ensure only one daemon start path exists (no overlapping systemd units/containers on the same ZEROCLAW_SOCKET).","Point ZEROCLAW_SOCKET at a private directory no other tooling manages."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Detect active interference before start: if the lock identity keeps changing\n// between two stats, something is replacing entries — investigate instead of racing it.\nfn lock_identity_stable(sock: &std::path::Path) -> Option<(u64, u64)> {\n    let mut s = sock.as_os_str().to_os_string();\n    s.push(\".lock\");\n    let p = std::path::PathBuf::from(s);\n    let a = std::fs::symlink_metadata(&p).ok()?;\n    std::thread::sleep(std::time::Duration::from_millis(50));\n    let b = std::fs::symlink_metadata(&p).ok()?;\n    (a.dev() == b.dev() && a.ino() == b.ino()).then_some((a.dev(), a.ino()))\n}","typeGuard":null,"tryCatchPattern":"Err(e) if e.to_string().contains(\"was replaced while being acquired\") => {\n    // something swaps the lock: stop scripts that rm/recreate the socket dir, then retry\n}","preventionTips":["Service units must create the socket dir if missing, never rm+recreate it.","Serialize daemon restarts (systemd dependency or a supervisor) so two starts cannot race.","Keep external tooling away from the daemon's private runtime dir."],"tags":["unix","ipc","lock-file","race-condition","startup"],"backgroundTag":"file-race-condition","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}