# PR #22825: live worktree-handoff evidence (Claude only)

- **Before** = `8d3b061121` (`ab1166d777^2`, main), detached worktree at `/tmp/ev-22825/before-wt`.
- **After** = `ab1166d777` (PR head) server code. The driver was committed afterwards as `2251684173` (it adds only `apps/server/scripts/verify-worktree-handoff-live.ts`; `git diff --stat ab1166d777 2251684173` lists that one file, so the server code is the same as `ab1166d777`).
- **Driver**: `apps/server/scripts/verify-worktree-handoff-live.ts --row <r1|r2|r3|r1b>`. Both sides ran the same driver and prompts. The driver spawned `apps/server/src/bin.ts start` from each side's checkout, with a fresh home and a fresh one-commit repo per row and side.
- **Environment**: machine `fs-33`, Linux 6.1.102 x86_64, Node v26.12.1. Claude provider `claudeAgent`, model `claude-sonnet-5-5`, effort `low`, for every parent and child. Traffic went through the CLIProxyAPI hub at `nucbox-1:8318`. No hub capacity errors and no retries. Captured 2026-10-22 11:10–11:19 UTC.

Times in "detach → X" are from domain-event `occurredAt` values. "New session attached" is the upper bound on when the old Claude session was released, because the fix releases the session after `turn.terminal` and only then opens the worktree session.

## R1: Claude parent → async Claude child; the child calls `t3_worktree_status`, then `t3_worktree_handoff` (continuation "Reply with the single word DONE…")

| | Before `8d3b061121` | After `ab1166d777` |
|---|---|---|
| Child run 1 | **failed** (11:12:23.954 → 11:12:33.336) | **interrupted** (11:12:22.797 → 11:12:30.949) |
| Child run 2 (continuation) | **queued, `queueHeld: true`, `startedAt: null`**, never started | starting → running → **completed**, text `DONE` |
| `provider-session.detached` | reason "Workspace changed." Run 1 failed +56 ms later | reason "Workspace changed." Turn `interrupted` +376 ms, new session attached +1255 ms, run 2 running +1289 ms |
| Continuation session cwd | none (no session) | `…/worktrees/repo-after-r1/handoff-r1-6969b1ee` |
| Server log | 1× `orchestration V2 provider event ingestion failed` with `ProviderAdapterEventStreamError` "Workspace changed." | 0 ingestion-failed lines, 0 `detach-interrupt-failed`, 0 `claude-query-interrupt-timeout` |
| Handoff tool item (child) | stuck `running` | `interrupted` |
| Parent delegated result | task **failed**: "The provider event stream closed unexpectedly…". Delivered and acknowledged; parent replied `ACK: failed` | task **completed**, result `DONE`. Delivered and acknowledged; parent replied `ACK: completed` |
| Notifications on parent | 1 | 1 |

## R2: R1 with continuation "Count from 1 to 400…", then interrupt the continuation

| | Before | After |
|---|---|---|
| Child run 1 | failed (+49 ms after detach) | interrupted (+62 ms after detach) |
| Continuation | Never started (`queueHeld: true`). The driver waited 15 s after run 1 ended, then dispatched **`queued-run.cancel`** (11:14:47.949). The run was removed from the projection; only run 1 (`failed`) remains | Started 11:14:28.925 (new session in `handoff-r2-34936ea6`, +984 ms after detach). The driver dispatched **`run.interrupt`** once it was `running`. Run 2 **interrupted** at 11:14:29.590 (0.67 s after start) |
| Parent delegated result | `failed`, "provider event stream closed unexpectedly". It had already been delivered and acknowledged before the cancel (`ACK: failed`). The cancel did not change it | task `interrupted`, "Child task ended with status interrupted.". Delivered once and acknowledged (`ACK: interrupted`) |
| Server log | 1 ingestion-failed line (Workspace changed.) | none; no hang |

`run.interrupt` on before cannot be tested because the continuation never runs. That is the expected reachability difference.

## R3: Claude top-level thread, no child, hands itself off (Claude **does** change here; only Codex is expected to be unchanged)

| | Before | After |
|---|---|---|
| Run 1 | **failed** (+51 ms after detach) | **interrupted** (turn +58 ms, run +67 ms) |
| Continuation (run 2) | **queued, `queueHeld: true`**, never started | **completed**, `DONE`. Session attached +1063 ms, running +1097 ms. cwd = `…/handoff-r3-86ee826f` |
| Server log | 1 ingestion-failed (Workspace changed.) | none |
| Handoff tool item | stuck `running` | `failed`, `toolNonExecutionKind: "user-rejected"` (see "what else changed") |

A smoke rerun with the final committed driver (`after/r3-final-script-smoke`) reproduced the after result: run 1 interrupted, run 2 completed `DONE` in the worktree, and clean logs.

## R1b: Claude parent starts an async Claude child ("Count 1 to 300"), then hands itself off while the child runs

| | Before | After |
|---|---|---|
| Parent run 1 | **failed** (+52 ms after detach) | **interrupted** (+77 ms) |
| Parent continuation (run 2) | **queued, `queueHeld: true`**, never started | **completed** (session attached +2058 ms, running +2103 ms, cwd = worktree) |
| Child | completed in the original repo cwd. Its lifecycle is the same on both sides, as the audit predicted | completed in the original repo cwd |
| Child result delivery | Delivered as parent **run 3**, which started and completed on a new session in the worktree **while run 2 stayed held**. Acknowledged (`ACK: completed`) | Delivered as parent run 3 after the continuation completed. Acknowledged (`ACK: completed`) |
| Server log | 1 ingestion-failed | none |

## What else changed between sides (besides the fix)

- **Model output varies between runs.** Turn lengths and timings differ: run 1 lasts about 8–13 s on each side, and the counting child stopped at about 43 numbers on both sides. Each side ran a separate Claude conversation through the hub. The before and after rows of a pair ran concurrently, within the same few minutes.
- **Code**: before is main `8d3b061121`. After is `ab1166d777`, the PR branch with that same main merged in, so the only server-code difference is the PR diff (`ProviderSessionManager.ts` and the new integration test).
- **Driver script**: the captures for all rows except the smoke rerun used a pre-commit draft of the driver. The draft and the committed version differ only in lint fixes (hoisted decoders, `Set`, `findLast`, the settling milestone). Prompts, the row logic and the server spawning are identical.
- **Handoff tool item status on after** is `interrupted` (R1) or `failed` with `toolNonExecutionKind: "user-rejected"` (R2, R3, R1b), although the handoff succeeded: the worktree was created, the thread moved and the continuation ran. The fix interrupts the turn while the MCP call is still in flight, and the Claude SDK reports the call as rejected. On before, the item stays `running` forever. This is cosmetic, but the transcript shows a successful handoff as a failed or rejected tool call. Worth a follow-up; not part of this PR's claim.
- **Before R1b**: the parent's own continuation is held, yet the child's completion delivery still started (run 3) and was acknowledged. So the audit's guess that the delivery "probably never reaches the parent" is **wrong** on this build. The held continuation is still lost.
- **Before R2**: the delegated result (`failed`) reached the parent before the driver cancelled the held run, so the cancel changed nothing the parent could see.
- **30 s detach wait**: never approached. The longest detach→turn-terminal on after was 376 ms (R1), and the longest detach→new session was 2.06 s (R1b).

## Unverified rows

- **R1-codex, R3-codex (controls)**: not captured. Codex is rate-limited on the hub tonight; per the brief, only Claude was used. The Codex multi-thread branch of `detach` is unverified at runtime.
- **R4** (metadata update, archive, or runtime-mode change on a running Claude turn without MCP): not in scope for this pass; not captured.

## Files

Per row and side, in `captures/<side>/<row>/`:

- `verdict.json`: final summarized projections for parent and child.
- `parent|child.projection.json`: full final projections.
- `parent|child.events.ndjson`: every subscription item with receive time.
- `*.states.ndjson`: deduplicated state changes.
- `timeline.txt` and `timing.json`: key domain events and detach timings.
- `milestones.ndjson`: driver actions.
- `prompt.txt`.
- `server.log`: debug level.
- `provider-logs/`: native Claude event logs.
