Symptom
When several hooks of the same turn run concurrently (parallel tool calls), all but one write a skip event:
{"kind":"skip","phase":"turn","reason":"turn diff incomplete: git could not snapshot the working tree in time"}
Root cause (measured)
Every hook in a turn snapshots through ONE shared scratch index: snapshotTree(root, path.join(turnDir(session, turn), "index")) (packages/cli/src/hooks/stop.ts, turnStart.ts). Concurrent git add -A runs under the same GIT_INDEX_FILE collide on index.lock; losers die in ~30ms with fatal: Unable to create '.../index.lock': File exists (nonzero exit, not a timeout). snapshotTree (packages/cli/src/lib/git.ts) returns undefined for that, and Stop maps every undefined to the same timeout message, so fast lock failures are indistinguishable from slow git.
Toggle matrix with the real snapshotTree (Apple Git 2.50.1):
- sequential, shared scratch, 3x: 3/3 ok
- 12 parallel, unique scratch each: 12/12 ok
- 6 parallel, shared scratch: 1/6 ok (repeated twice)
Isolated snapshots take 50-260ms vs the 5s/8s budgets, so this is purely the lock race, in any checkout (main or fresh worktree).
Suggested fix
Use a per-invocation scratch path (e.g. index.<pid>) or retry git add past a transient lock, and give lock contention its own skip message so it stops masquerading as slow git.
Repro: copy any repo index to a scratch path, then run 6x GIT_INDEX_FILE=<scratch> git add -A -- . in parallel — 5 fail with the lock error.
Symptom
When several hooks of the same turn run concurrently (parallel tool calls), all but one write a skip event:
{"kind":"skip","phase":"turn","reason":"turn diff incomplete: git could not snapshot the working tree in time"}Root cause (measured)
Every hook in a turn snapshots through ONE shared scratch index:
snapshotTree(root, path.join(turnDir(session, turn), "index"))(packages/cli/src/hooks/stop.ts,turnStart.ts). Concurrentgit add -Aruns under the sameGIT_INDEX_FILEcollide onindex.lock; losers die in ~30ms withfatal: Unable to create '.../index.lock': File exists(nonzero exit, not a timeout).snapshotTree(packages/cli/src/lib/git.ts) returns undefined for that, and Stop maps every undefined to the same timeout message, so fast lock failures are indistinguishable from slow git.Toggle matrix with the real
snapshotTree(Apple Git 2.50.1):Isolated snapshots take 50-260ms vs the 5s/8s budgets, so this is purely the lock race, in any checkout (main or fresh worktree).
Suggested fix
Use a per-invocation scratch path (e.g.
index.<pid>) or retrygit addpast a transient lock, and give lock contention its own skip message so it stops masquerading as slow git.Repro: copy any repo index to a scratch path, then run 6x
GIT_INDEX_FILE=<scratch> git add -A -- .in parallel — 5 fail with the lock error.