Daemon lock sites omit onCompromised, so a stolen lockfile throws from an fs callback and kills the supervisor #1556
Jiaaqiliu
started this conversation in
Bug reports
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Environment. prime-agent 0.7.3,
mainat f8f0036, Node 22.17.0, macOS 15 (darwin 25.5.0).Summary
proper-lockfiledefaultsonCompromisedto(err) => { throw err; }and calls it from inside the lock's mtime-refresh filesystem callback, so the throw is an uncaught exception rather than something the caller can handle. None of the daemon's four lock sites pass the option.The socket-path lease is the one that matters: the supervisor holds it for its whole lifetime and installs no
uncaughtExceptionhandler, so a lock steal kills the supervisor and takes every session's control plane with it.Root cause
node_modules/proper-lockfile/lib/lockfile.js:213sets the default, and:184-200(setLockAsCompromised) invokes it from thefs.stat/fs.utimescallback.Sites without the option:
modes/daemon/daemon-socket.ts:50— the socket-path lease,stale: 5000,update: 1000modes/daemon/daemon-socket.ts:171— the sync cleanup lockmodes/daemon/daemon-supervisor-ownership.ts:359— registry guardcli/daemon-update-restart.ts:327— coordinator guardcore/session-lease.ts:192— lease guardcore/auth-storage.ts:196already passesonCompromisedand records the loss in a flag it checks later, which is what the correct handling looks like.Why the lease gets stolen
acquireDaemonSocketPathLeaseis called atdaemon-supervisor.ts:652and released only at:5134/:5215. The mtime refresh is asetTimeout, so it cannot fire while the event loop is blocked — and the supervisor blocks it routinely:execFileSync("ps")for process identity (core/session-lease.ts:159),Atomics.wait(..., 10)up to 100 times in the lease guard (:205),readFileSyncjournal loads. A stall past the 5sstalewindow lets a second supervisor'sacquireDaemonSocketPathLeasedeclare the lock stale,rmdirit, and take over.Reproduced with the exact production options by blocking the event loop for 6s while a second holder takes the lock:
A repo-wide grep finds
uncaughtException/unhandledRejectionhandlers only inmodes/daemon/daemon-mode.ts:595-599, i.e. in the worker, not in the supervisor.Suggested fix
Record the loss on the lease instead of throwing, and act on it where it actually matters:
prepareDaemonSocketPathfails startup with a clear message, andcleanupDaemonSocketPathskips the unlink, because the socket at that path may already belong to the successor that stole the lease. The other three sites take a non-throwing handler for the same reason: nothing may throw from that callback.I have a fix with a regression test on a branch:
fix/lockfile-compromise-crash.npm run checkpasses and the surrounding suites still pass. I opened it as a PR first and the contribution gate closed it, which is what CONTRIBUTING.md says should happen, so I am bringing it here instead. Happy to leave it as is, adjust it, or drop it entirely if you would rather fix this differently.All reactions