Skip to content

fix(server): recovering a pruned worktree no longer wedges every thread - #81

Merged
asfires merged 1 commit into
mainfrom
fix/worktree-recovery-lease-deadlock
Sep 18, 2026
Merged

asfires merged 1 commit into
mainfrom
fix/worktree-recovery-lease-deadlock

Conversation

@asfires

@asfires asfires commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Problem

Sending a message to a thread whose managed worktree had been pruned left that thread stuck on "Working" forever, and then every other thread's next turn got stuck behind it. Only a server restart cleared it.

A turn start holds the worktree's workspace lease, a one-permit semaphore that is not reentrant. With the worktree missing, the recovery path re-created it and ran the setup script from inside that lease. Running the script opens a terminal, and terminalManager.open takes the same lease, so the turn start waited on itself. The reactor worker is serial, so all later turn starts queued behind the hung one and no provider process was ever spawned.

The lease came from upstream's storage cleanup change and the recovery path is fork-local. Each is fine alone.

Fix

The recovery setup script runs in a forked fiber, so it takes the lease once the turn start releases it. A setup failure is logged instead of failing the turn.

The recovery test missed this because its setup runner mock never took the lease. The mock now takes it like the real terminal manager does. It reproduced the hang before the fix and passes after.

Verifying

vp test run src/orchestration/Layers/ProviderCommandReactor.test.ts in apps/server passes. In a running app: send a message to a thread whose worktree was pruned. It should start a provider session and a setup terminal instead of hanging.

Model: Claude Fable 5.1 via Claude Code.

A turn start holds the worktree's workspace lease, a one-permit semaphore
that is not reentrant. When the thread's managed worktree had been pruned,
the recovery path re-created it and ran the setup script from inside that
lease. Running the script opens a terminal, which takes the same lease, so
the turn start waited on itself forever. The reactor worker is serial, so
every later turn start on any thread queued behind it and the threads sat on
"Working" with no provider process.

The setup script now runs in a forked fiber, so it takes the lease once the
turn start releases it. A setup failure is logged instead of failing the
turn.

The recovery test missed this because its setup runner mock never took the
lease. It does now, and it reproduced the hang before the fix.

Model: Claude Fable 5.1 via Claude Code.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S labels Sep 18, 2026
@asfires
asfires merged commit f178f00 into main Sep 18, 2026
6 checks passed
@asfires
asfires deleted the fix/worktree-recovery-lease-deadlock branch September 18, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 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