Add worktree locking to prevent concurrent agent access#463
Open
mattpocock wants to merge 5 commits intomainfrom
Open
Add worktree locking to prevent concurrent agent access#463mattpocock wants to merge 5 commits intomainfrom
mattpocock wants to merge 5 commits intomainfrom
Conversation
…closes #428 Task: #428 (WorktreeLock module + createWorktree happy path) PRD: part of worktree-locking issue series Key decisions: - acquire() uses O_EXCL atomic file creation; writes {pid, branch, acquiredAt} JSON - release() is idempotent (swallows ENOENT) - lock skipped on WorktreeManager reuse path (lock file already exists = another handle owns it) - release() called unconditionally in close() before dirty check, wrapped in .catch(() => {}) - lockDir (.sandcastle/locks/) created on first acquire() Files changed: - src/WorktreeLock.ts (new): acquire() + release() + LockData type - src/WorktreeLock.test.ts (new): 5 unit tests (create file, remove file, idempotent, fail if exists, create dir) - src/createWorktree.ts: import acquire/release/basename, wire acquire after create(), wire release in close() - src/createWorktree.test.ts: integration test (lock exists after create, gone after close) - .changeset/worktree-lock-module.md: patch changeset Notes: WorktreeLockError type deferred to #429 per issue spec; reuse-path skip uses existsSync TOCTOU-safe for single-process concurrency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…429 Task: #429 (WorktreeLock contention detection) PRD: part of worktree-locking issue series Key decisions: - acquire() reads lock JSON on EEXIST, checks PID liveness via process.kill(pid, 0) - Live PID → throws WorktreeLockError with owningPid, branch, timestamp diagnostics - Dead PID → removes stale lock file, retries atomic O_EXCL creation - Corrupt/unreadable lock file treated as stale (removed + retried) - Lock acquisition moved before WorktreeManager.create for named branches to serialize concurrent access; merge-to-head still locks after create (unique names, no contention possible) - WorktreeLockError added to errors.ts following existing TaggedError pattern - ErrorHandler.ts updated for exhaustive SandboxError handling Files changed: - src/errors.ts: add WorktreeLockError (TaggedError with owningPid, branch, timestamp) - src/ErrorHandler.ts: add WorktreeLockError to formatErrorMessage switch + catchTags - src/WorktreeLock.ts: enhance acquire() with PID liveness check + stale recovery - src/WorktreeLock.test.ts: 3 new tests (live PID contention, dead PID recovery, concurrent race) - src/createWorktree.ts: lock before create for named branches, release on create failure - src/createWorktree.test.ts: concurrent lock contention integration test, updated reuse tests - .changeset/worktree-lock-contention.md: patch changeset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract the duplicated "remove stale + retry O_EXCL + handle race"
pattern into a removeStaleAndRetry helper. This eliminates:
- Duplicated retry logic for corrupt-lock and dead-PID paths
- The `lockData = undefined as unknown as LockData` type escape hatch
- Unnecessary `fd!` non-null assertions (fd is now properly typed)
- An inline `import("node:fs/promises").FileHandle` type import
No behavior change — all existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
#430 Task: #430 (pruneStale lock cleanup) PRD: part of worktree-locking issue series (#427) Key decisions: - pruneStale(lockDir, activeWorktreeNames) scans .sandcastle/locks/ for .lock files - Orphaned locks (worktree not in active set) → removed - Dead-PID locks (active worktree but owning process dead) → removed - Live locks (active worktree + live PID) → preserved - Missing lockDir handled gracefully (ENOENT → no-op) - Wired into WorktreeManager.pruneStale() after orphaned directory cleanup - Active worktree names derived from git worktree list basenames Files changed: - src/WorktreeLock.ts: add pruneStale() export, add readdir import - src/WorktreeLock.test.ts: 4 unit tests (orphaned, dead PID, live, missing dir) - src/WorktreeManager.ts: import pruneStaleLocks, call after directory cleanup - src/WorktreeManager.test.ts: integration test (stale lock from simulated crash) - .changeset/prune-stale-locks.md: patch changeset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ir missing The early return on missing .sandcastle/worktrees/ also skipped lock file pruning. Move the git worktree list fetch before the directory check and convert the early return to a conditional block so lock pruning always runs. Also mark LockData fields readonly for consistency with the rest of the codebase. Co-Authored-By: Claude Opus 4.6 <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.
Implements PRD #427: file-based locking to prevent two processes from operating on the same worktree simultaneously.
Introduces a
WorktreeLockmodule withacquire(),release(), andpruneStale(). Lock files live at.sandcastle/locks/<worktree-dir-name>.lockwith JSON content ({ pid, branch, acquiredAt }). Atomic creation viaO_EXCLprevents races. PID liveness checking viaprocess.kill(pid, 0)detects stale locks from crashed processes.Wired into
createWorktree()— lock acquired after worktree creation, released unconditionally onclose()/dispose.WorktreeManager.pruneStale()cleans up stale locks alongside orphaned worktree directories.Closes #427, #428, #429, #430.
Closes #428
Closes #429
Closes #430