fix(daemon): Allow a single daemon per run-agent dir - #125
Merged
Merged
Conversation
A CLI whose 1s socket probe timed out against a busy daemon spawned another one, which unlinked the live socket and reattached the same sessions. Stray daemons kept writing to SQLite, so the event loop hit SQLITE_BUSY_SNAPSHOT and marked live sessions failed with "database is locked". One machine had 10 daemons running. The daemon entry now takes an exclusive SQLite lock on daemon.lock before opening the database. The kernel drops it on any exit, SIGKILL included, so there is no stale lock to clean. A losing daemon logs and exits 0, and the spawning CLI keeps polling the running one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6o1wJTTJWcHm9mZMGvDTn
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Problem
One machine had 10
dist/daemon/daemon.js --daemonprocesses running on the same~/.run-agent. Only the newest owned the socket. The other nine kept tailing session logs and writing to SQLite.How it happens:
isDaemonRunning()gives up on the socket after 1s.DatabaseSync, so a long query, checkpoint orbusy_timeoutwait blocksaccept.ensureDaemonStarted()then spawns a second daemon.createIpcServer()unlinks the live socket, and the new daemon'srecover()reattaches the same sessions.With several daemons writing, the deferred
BEGINinattachDriverEventsfails immediately withSQLITE_BUSY_SNAPSHOT(errcode 517), whichbusy_timeoutdoes not wait on. Thecatchthen marks the sessionfailedwithdatabase is lockedand leaves the event loop, while the harness is still running (seen on acodex exec resumeturn).Change
src/daemon/instance-lock.ts:acquireInstanceLock()holdsdaemon.lockas a small SQLite file inlocking_mode=EXCLUSIVEwithbusy_timeout=0.run-agent.dbwould block read-only CLIs.--daemonentry takes the lock beforenew Daemon(), so a losing daemon never migrates, recovers or touches the socket. It logs one line todaemon.logand exits 0, and the spawning CLI keeps polling the daemon that already runs.getPaths().daemonLock, plus the spec in.specs/features/daemon-single-instance/spec.md.Validation
tests/daemon-instance-lock.test.ts(5 tests, written first and seen red): same-process refusal, cross-process refusal, release after SIGKILL, release afterrelease().dist/with two daemons on oneRUN_AGENT_DIR:daemon.pid;kill -9on the first, a new daemon starts.open-claude-probes"probes again when the binary size changes") also fails without this diff.Follow-up
The busy-store handling in the event loop is a separate PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_01R6o1wJTTJWcHm9mZMGvDTn