Conversation
acquireRunnerLock: owner-only parent directory, no-follow identify or create, refuse hard links and symlinks, then an OS lock on ~/.codeboost/locks/<dev>-<ino>.runner-lock (SQLite exclusive locking) that the OS releases on exit or crash; verify() after the Store opens. The CLI takes the lock before starting and releases it after shutdown. recoverStartup: stop leftover preparation groups, D recovery (injected until #51), export stopped writable attempts with a deadline, one finalization transaction (settlement precedence, cancel and time-limit guards, requeue claim), storage removal, attempt-directory sweep, merged-task repair, and a fail-closed stop for unrecorded preparation. Store: runner owner token, preparation and allocation markers, requeue claim at admission, and releasePreparation with an open-files check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A caller that dropped the returned RunnerLock let its SQLite connection be garbage-collected, which released the OS lock while the runner was still alive. Found by a CI failure of the cross-process lock test on #60. The new test drops the lock and forces GC in a child, and fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Lane F, step F1, slice F1d: startup recovery and the single-runner lock. Stacked on #57 (F1c) → #56 (F1b) → #53 (F1a). Related: #22, #51.
What this does
Single-runner lock (
runner/recovery.ts,acquireRunnerLock; decision 1):O_NOFOLLOW. If it doesn't exist, create it withO_CREAT|O_EXCL|O_NOFOLLOW(mode 0600), handling a creation race. Refuse symlinks, non-regular files, and files with more than one hard link.~/.codeboost/locks/<device>-<inode>.runner-lock. It is a small SQLite file held inlocking_mode=EXCLUSIVEafter a first write, and the OS releases it on exit or crash. There is no stale-lock takeover, and lock files are never deleted. A renamed database, or one moved to another directory on the same filesystem, meets the same lock; a copy gets its own.verify()checks, after the Store opens, that the path still names the locked file.The CLI now takes the lock before starting the server and releases it after shutdown. A second start prints "Another codeboost runner is using this database." and exits 1 (smoke-tested with
npm run demotwice).Startup recovery (
recoverStartup), the contract's steps 2b–7, fail-closed at each step:recoverLeftovers(runnerOwner), until D follow-ups required by the F1 runner lifecycle contract #51 delivers it. A rejection, or older-build resources without a runner label, stops startup before any row is finalized.<attemptId>.diffnames, a 60 s abortable deadline, and failures recorded as diagnostics. Then one finalization transaction (Store.recoverInterrupted):stale, then an expired budget givingtime-limit, then an expired deadline giving "Timed out", then "Interrupted";shutdownreason;requeue_pending) is set for shutdown-cancelled or interrupted tasks.<runnerRoot>/<token>/attempts: owned entries are removed; symlinks, other devices and unknown names are reported and left alone.task-closedevent.releasePreparationremoves its directory only if no process has files open or a working directory there (lsof +Don macOS,/procon Linux), failing closed if that check can't run.Store additions:
runnerOwnerToken(file): a 32-hex token tied to the database file's device and inode. A copy gets a new token; a malformed stored value refuses startup.markPreparationStarting,recordPreparationGroup,cancelPreparationStart) andrecordAllocation, where F chooses the allocation ID.admitAttempt({ claimRequeue }): the requeue claim is cleared by compare-and-swap in the admitting transaction, and a second claim is refused.Validation (head
cfba95b)npm run typecheck: passes.test/runner-recovery.test.ts:npm run test:browser: 53 passed. The browser tests callstartServerdirectly, so the CLI lock was smoke-tested by hand.CI fix after opening (
5d163f2)The cross-process lock test failed once on CI (surfaced on #60). The cause is real: a caller that drops the returned
RunnerLocklets its SQLite connection be garbage-collected, and that silently releases the OS lock. Held connections are now kept in a module-level set untilrelease(). The new test "keeps holding the lock when the caller drops the returned object and garbage collection runs" fails without the fix and passes with it (3 of 3 runs).Limits and follow-ups
recoverLeftoversand the storage handles from D follow-ups required by the F1 runner lifecycle contract #51. Until then there is no runner in production, so no attempt can be left running.--list-unowned-agent-resourcesalso waits for D follow-ups required by the F1 runner lifecycle contract #51. It needs D's report.🤖 Generated with Claude Code