Skip to content

fix(server): worktree launches no longer fail when the first git call is slow - #174

Merged
jayleaton merged 1 commit into
mainfrom
fix/concurrent-worktree-create
Oct 30, 2026
Merged

jayleaton merged 1 commit into
mainfrom
fix/concurrent-worktree-create

Conversation

@jayleaton

Copy link
Copy Markdown
Owner

Summary

Launching several threads into worktrees at once, or launching the first thread on a new machine, could fail with Failed to resolve the VCS driver. The launch then had no worktree.

The cause was not a race. Repository detection capped its first git call at 5 s, while every git call after it gets the 30 s default. The first git process to touch a folder is the one that absorbs any first-contact stall:

ThreadLaunchService.prepareInBackground            (also WorktreeMcpService, ws createWorktree)
└─ VcsDriverRegistry.resolve → detect               (Cache: concurrent launches share ONE call)
   └─ GitVcsDriver.detectRepository
-     └─ git rev-parse --is-inside-work-tree   timeout 5 s    ← macOS privacy prompt holds it here
+     └─ git rev-parse --is-inside-work-tree   timeout 30 s (default, same as the next two calls)
      ├─ git rev-parse --show-toplevel           30 s
      └─ git rev-parse --git-common-dir          30 s

Because detection is cached per repository, one stalled call failed every concurrent launch in that repository together. That is why this looked like a concurrency bug.

The cap dates from the original VCS driver code (pingdotgg#2435), with no stated reason. If the prompt or the stall lasts longer than 30 s, the launch still fails.

Root cause from the field (studio-01, nightly 0.1.3-…432001, a fresh install)

time (UTC) event
10:17:56.672 Doug's launch spawns git rev-parse --is-inside-work-tree in ~/Documents/projects/agent-smoke: T3's first git call under ~/Documents
10:17:56.678 sandboxd sends a TCC request for git (responsible: com.jayleaton.t3agents); kTCCServiceSystemPolicyDocumentsFolder shows the consent prompt
10:18:01.67 the 5 s timeout kills git, and the launch fails during provision-worktree
10:18:05.685 the user clicks Allow: kTCCServiceSystemPolicyDocumentsFolder granted by TCC for git
10:18:14 Cody launches into the same repository and succeeds; consent was already granted

The two launches never overlapped. The parallel-worktree .git/config lock race was already fixed by pingdotgg#25700, which is in that build. GitVcsDriverCore.test.ts › "prepares concurrent worktrees without losing their branch config" (12 concurrent worktrees on one repository) passes on this branch.

Evidence

The new test is GitVcsDriver.test.ts › "detects a repository while git waits on a privacy prompt". A fake spawner holds --is-inside-work-tree until the test releases it, and TestClock advances 10 s. It exercises the real ProcessRunner timeout and uses no wall-clock waits.

  • Before (fix reverted, Davit box):
    × GitVcsDriver detects a repository while git waits on a privacy prompt
    VcsProcessTimeoutError: VCS process timed out in GitVcsDriver.isInsideWorkTree: git (/repo) after 5000ms
    
    This is the same error as the studio trace.
  • After:
    • vp test run src/vcs/GitVcsDriver.test.ts src/vcs/VcsDriverRegistry.test.ts: 60 passed.
    • GitVcsDriverCore.test.ts -t "concurrent worktrees": 1 passed.
    • vp fmt --check and vp lint on the changed files: clean.
  • check:changed could not run in the box, which has no git history. Typecheck results are in the thread notes; CI runs the full typecheck.

Merge Danger

Door: two-way. This is a single timeout value; reverting it restores the old behavior.

Blast Radius: server

Detection now waits up to 30 s instead of 5 s before reporting a timeout, and only when git really hangs (a dead network mount, an unanswered prompt). Callers that hit an uncached detection, such as status streams and launches, wait longer in that case. Normal detection takes about 10 ms and is unchanged.

Follow-up, not in this PR

After a failed worktree setup, the thread keeps worktreePath: null. A follow-up message sent to start right away (for example t3_thread_send) runs in the project checkout. Queued messages are already held in this case; a direct start is not. That is how Doug's retry ended up committing in the root checkout.

Model: Claude Opus 5.5 via Claude Code in T3 Code.

🤖 Generated with Claude Code

… is slow

Repository detection capped `git rev-parse --is-inside-work-tree` at 5 s while
every git call after it gets the 30 s default. The first git process to touch a
folder absorbs any first-contact stall: macOS holds it until the user answers
the "access files in your Documents folder" prompt, and a cold disk or a burst
of launches can do the same. Detection is shared, so one stalled call failed
every concurrent launch in that repo. Detection now uses the default timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XS labels Oct 30, 2026
@jayleaton
jayleaton marked this pull request as ready for review October 30, 2026 10:56
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.6 KiB 21.6 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.1 KiB 5.0 KiB −6 B (−0.1%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.3 KiB −6 B (−0.5%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: a74ab4f · PR result: e5f74ae · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@jayleaton
jayleaton merged commit 4883e15 into main Oct 30, 2026
31 checks passed

@jayleaton jayleaton left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MERGE — reviewed e5f74ae against original base a74ab4f; the reviewed files match the already-merged 4883e15 exactly.

No new correctness, security, performance, or concurrency findings. Removing the 5-second override inherits the existing 30-second VcsProcess limit. Process concurrency, scoped cancellation, detection-cache deduplication, and retry after failed detection remain intact. A genuinely stalled probe can wait 25 seconds longer, as documented.

Read AGENTS.md, CLAUDE.md, CONTRIBUTING.md, package scripts and Vite/TypeScript configuration. Local checks through heavy.sh:

  • Repository scripts/check-changed.ts: all pass (format, lint, both Knip passes, GraphQL, three-package typecheck, 57 adjacent tests).
  • GitVcsDriver/GitVcsDriverRegistry/VcsProcess: 88 tests pass.
  • Existing concurrent/parallel worktree tests: 2 pass.
  • New regression: restoring the old timeout fails with VcsProcessTimeoutError after 5000ms; restoring this head passes.
  • git diff --check passes; the checkout is restored and clean.

The default check runner exceeded the 7 GiB scope and initially hit a harness temporary-path socket limit. The successful run used Node 26.12.1, a shorter temporary path, and serialized unchanged vp child commands; no checks were removed or source files changed. No macOS privacy prompt was exercised locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant