fix(server): Claude child continues after a worktree handoff#22825

draft8
main
fix/issue-15136
opened15d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Fixes #15136.

On e27a03e94d, moving a Claude child with t3_worktree_handoff fails its first run and leaves continuation run 2 queued with queueHeld=true and no start time. The Codex control continues and writes its file.

Evidence pending. Before runtime transcript.

Implementation, scope assessment for the related stale handoff banner, and after evidence pending.

Model and harness: GPT-6.1-Sol through Codex in T3 Code.

Evidence

Head captured: after = ab1166d777 (this branch with main merged in), before = 8d3b061121 (main, the merge base). The PR head, 2251684173, only adds the driver, apps/server/scripts/verify-worktree-handoff-live.ts; the server code is identical to ab1166d777.

Fixture matrix (proof audit pass 1): audit1-matrix.md. A Claude child never shares its parent's provider session, so the bug only shows when the thread that calls t3_worktree_handoff is on Claude. R1 is the child handing itself off (the issue repro); R1b is the brief's literal "parent hands off while its child runs".

RowBefore (8d3b061121)After (ab1166d777)
R1: Claude child calls t3_worktree_handoffChild run 1 failed. Server logs ProviderAdapterEventStreamError … Workspace changed.. Continuation run 2 stays queued, queueHeld:true, never starts. Parent receives failed.Child run 1 interrupted. Run 2 completed ("DONE") on a new session whose cwd is the worktree. Parent receives completed. No ingestion warnings.
R2: user interrupts the child's continuationRun 2 never starts, so it can't be interrupted. The driver cancels the held queued run instead; the parent already had failed.run.interrupt → run 2 interrupted 0.67 s after it started. Task interrupted, delivered once, no hang.
R3: top-level Claude thread hands off, no childRun 1 failed, continuation held.Run 1 interrupted 67 ms after detach, continuation completed in the worktree. (Claude changes here too; only Codex is unchanged.)
R1b: Claude parent hands off while its Claude child runsParent run 1 failed, continuation held. The child's result still arrives as parent run 3.Parent run 1 interrupted, continuation completed, then the child's result arrives and is acknowledged.

After timings: 51–376 ms from detach to the turn being interrupted; 0.98–2.06 s from detach to the new worktree session. The new 30 s terminal wait was never close to being used.

Captures (ghdrop upload -m output): captures.md captures-pr-22825.tar.gz r1-before-child-timeline.txt r1-after-child-timeline.txt

Environment: fs-33 (Linux 6.1, x86_64), Node v26.12.1. A real dev server per side with isolated --home-dir under /tmp and a throwaway git repo per side. Real Claude turns (claude-sonnet-5-5, effort low) through the CLIProxy hub; prompts forbade every tool except the named T3 MCP tools.

What else changed between sides:

  • 129 commits of main came in through the merge. The before side is that same main, so both sides share them.
  • Model output is nondeterministic. Only the run states and lifecycle events are compared, not the text, beyond "DONE" or an ACK.
  • On after, the t3_worktree_handoff tool item ends failed/user-rejected in R2, R3 and R1b and interrupted in R1, even though the handoff succeeds. On before it stays running forever. The label comes from the Claude adapter marking a tool call as not executed when its turn is interrupted mid-call. It's cosmetic, and the audit found it isn't a blocker; it's worth a follow-up.
Verification log
  • Proof audit pass 1: matrix derived from the diff (audit1-matrix.md). It corrected the brief: a parent's handoff can't touch a Claude child, R3 changes for Claude, and R2 can't be reached on before.

  • Proof audit pass 2: SUFFICIENT (audit2.md).

    • Every row was checked against the raw projections, milestones and server logs.
    • git diff ab1166d777 2251684173 -- apps/server/src apps/web/src packages is empty.
    • The before SHA was pinned by matching the stack-trace line numbers to 8d3b061121; the after SHA by the worktree reflog against the capture timestamps.
    • After-R3 was rebuilt from the committed driver on a fresh isolated server: run 1 interrupted 71 ms after detach, run 2 completed in the worktree, no warnings.
    • The draft and committed versions of the driver couldn't be diffed (the draft is gone); the rebuild covers that gap.
  • Tests: vp test run src/orchestration-v2/SelectionRestart.integration.test.ts src/orchestration-v2/ProviderSessionManager.test.ts on the merged head: 2 files, 50 tests passed. vp lint and vp fmt --check pass on the three changed files.

  • CI (cafe pr status at 2251684173): 25 passing, 0 pending, 2 failing (Lint, Check). cafe ci returns no runs for this repo, so I couldn't read the failing logs or confirm which commit they ran on. The changed files lint clean locally.

  • Not verified:

    • R1-codex and R3-codex controls: Codex was rate-limited, so the multi-thread branch of detach is unchecked at runtime.
    • R4 (archive or runtime-mode change on a running Claude turn, which goes through the same path): out of scope.
  • Model and harness: evidence lead, auditors and capturer were Claude Opus 5.5 (claude-opus-5-5) through Claude Code in T3 Code; the agents inside the fixture were claude-sonnet-5-5.

  • Takeover CI fix and review round (2026-10-22):

    • CI cause: Lint and Check failed on 2251684173 because knip reported apps/server/scripts/verify-worktree-handoff-live.ts as an unused file. Local knip --include files,dependencies showed that same file as the only finding.
    • Fix: 9767a4c7f3 registers the script as an apps/server entry in knip.jsonc, next to verify-background-live.ts. With that entry, both the files/dependencies knip pass and the apps/server exports pass are clean. CI on the new head is pending.
    • Review A, Claude Sonnet 5.5 (claude-sonnet-5-5): VERDICT: MERGE. No failing sequence, and no auth, permission, mode, sandbox, trust or MCP surface is touched. It notes that other exclusive adapters could wait the full 30s if their interruptTurn no-ops on a turn that has already ended (bounded). The failed/user-rejected label on the tool call isn't blocking and deserves a follow-up.
    • Review B, Claude Opus 5.5 (claude-opus-5-5): VERDICT: MERGE. No failing sequence and no security surface touched. Nit: DETACH_TURN_TERMINAL_TIMEOUT_MS (30s) equals the effect worker's 30s lease, so a wedged adapter could outlast the lease; it suggests 15s and calls this theoretical. Same view on the tool-call label.
    • Both reviewers question keeping the 435-line evidence driver in the repo. Neither treats it as blocking.
    • Model and harness: Claude Opus 5.5 (claude-opus-5-5) through Claude Code in T3 Code; reviewers as listed.

Fixes #15993 GitCafe: https://git.cafe/pingdotgg/t3code/issues/15993

Checks

3 failing · 19 successful · 6 skipped
Some checks failed

Failing3

a9dc2e3

Added 1 commit

  • test(server): add takeover tests for delegated worktree handoff5544eba
Synced from GitHub

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for a9dc2e3.

This comment will update automatically after the next completed run.

Added 7 commits

  • fix(server): interrupt exclusive turns before worktree detachb0bce18
  • Merge remote-tracking branch 'origin/main' into fix/issue-15136ab1166d
  • test(server): add worktree handoff evidence driver2251684
  • chore(server): register worktree handoff evidence driver with knip9767a4c
  • Merge remote-tracking branch 'origin/main' into HEAD413af24
  • Merge remote-tracking branch 'origin/main' into HEADed73265
  • Merge branch 'main' into fix/issue-15136a9dc2e3