# PR #22825 proof audit, pass 2

**Verdict: SUFFICIENT** for the PR's claim, which covers Claude (exclusive-session) handoff, delegated or not. Codex controls (R1-codex, R3-codex) and R4 were never captured. They cover the unchanged multi-thread branch and scope beyond the handoff, so they are not missing rows for this claim. Note that they are unverified at runtime.

## Provenance

- `git diff ab1166d777 2251684173 -- apps/server/src apps/web/src packages`: **empty** (0 bytes). The full diff stat is the single file `apps/server/scripts/verify-worktree-handoff-live.ts` (+435).
- `ab1166d777^2` = `8d3b061121fe…`, confirmed. The `8d3b061121..ab1166d777` stat is `ProviderSessionManager.ts` (+40/−14) and `SelectionRestart.integration.test.ts` (+251).
- **How the capture recorded which code each server ran:** only by path, in `milestones.ndjson` → `server-spawned.root`. Every before row has `/tmp/ev-22825/before-wt`. Every after row has `/home/ubuntu/wt/pr-22825`. No SHA was logged, and `before-wt` has since been deleted. Independent fingerprints:
  - **Before:** the before server logs' stack traces cite `before-wt/…/ProviderSessionManager.ts:532:23` (`new ProviderAdapterEventStreamError(`) and `:1882:38` (`runtime.interruptTurn`). Both match `8d3b061121` exactly. At `ab1166d777` the same statements sit at 533 and 1883, shifted by the new `DETACH_TURN_TERMINAL_TIMEOUT_MS` line. So before ran pre-fix code.
  - **After:** the `pr-22825` reflog shows HEAD = `ab1166d777` from 10:59:00 until the driver commit `2251684173` at 11:19:52. Every after capture falls between 11:10 and 11:18, inside that window. The working tree is clean now. The after logs contain no stack traces to fingerprint. Behaviour (run 1 `interrupted`) is only possible with the fix.
- **Draft vs committed driver:** **not checkable**. No copy of the draft remains under /tmp/ev-22825 or in the tarball. My rebuild below used the committed driver at `2251684173` and reproduced the after R3 result, which covers this gap for R3. Note: the smoke rerun (11:18:20) predates the commit (11:19:52), so it ran the final working copy, not provably the committed blob.

## Per-row audit (claims in captures.md vs raw `*.projection.json`, `milestones.ndjson`, `server.log`)

| Row | Side | Raw run statuses (projection) | Handoff tool item | Continuation session cwd | `ingestion failed` (Workspace changed.) | Driver action | Matches captures.md |
|---|---|---|---|---|---|---|---|
| R1 (child) | before | 1 `failed`, 2 `queued` `queueHeld:true` | `running` | no session | 1 (`ProviderAdapterEventStreamError` → `Error: Workspace changed.`) | — | ✅ (parent sub `failed`, `ACK: failed`) |
| R1 (child) | after | 1 `interrupted`, 2 `completed` `DONE` | `interrupted` | `handoff-r1-6969b1ee` | 0 | — | ✅ (parent sub `completed`, `ACK: completed`) |
| R2 (child) | before | 1 `failed` (run 2 removed after cancel) | `running` | none | 1 | `queued-run.cancel` 11:14:47.949 | ✅ |
| R2 (child) | after | 1 `interrupted`, 2 `interrupted` | `failed`/`user-rejected` | `handoff-r2-34936ea6` | 0 | `run.interrupt` 11:14:29.579 | ✅ (sub `interrupted`, `ACK: interrupted`) |
| R3 (top) | before | 1 `failed`, 2 `queued` `queueHeld:true` | `running` | no session | 1 | — | ✅ |
| R3 (top) | after | 1 `interrupted`, 2 `completed` `DONE` | `failed`/`user-rejected` | `handoff-r3-86ee826f` | 0 | — | ✅ (timing.json: turn +58 ms, run +67 ms, session +1063 ms) |
| R1b (parent) | before | 1 `failed`, 2 `queued` `queueHeld:true`, 3 `completed` (delivery) | `running` | `handoff-r1b-b6e07a3b` (run 3 only) | 1 | — | ✅ (child completed in the repo cwd; delivery acknowledged as run 3 while run 2 stays held) |
| R1b (parent) | after | 1 `interrupted`, 2 `completed`, 3 `completed` | `failed`/`user-rejected` | `handoff-r1b-c3c809dc` | 0 | — | ✅ |
| R3 smoke | after | 1 `interrupted`, 2 `completed` `DONE` | — | `handoff-r3-cff5e911` | 0 | — | ✅ |

No `detach-interrupt-failed` lines on any side. No discrepancies between captures.md and the raw files.

## Rebuild: after-R3 with the committed driver

`node apps/server/scripts/verify-worktree-handoff-live.ts --row r3 --root /home/ubuntu/wt/pr-22825 --home /tmp/ev-22825/audit2/home-r3 --repo /tmp/ev-22825/audit2/repo-r3 --out /tmp/ev-22825/audit2/r3`. HEAD was `2251684173` with a clean tree, fresh home and fresh repo, Claude `claude-sonnet-5-5`, effort low. Exit 0. The driver spawned server pid 1573059 and stopped it itself (`server-stopped`); `ps` confirms it is gone.

- Run 1 **`interrupted`** (11:22:55.016 → 11:23:03.697). `provider-session.detached` "Workspace changed." at 11:23:03.626, so run 1 ended **+71 ms** after the detach.
- Run 2 **`completed`**, text `DONE` (11:23:04.889 → 11:23:08.027). The new session attached at 11:23:04.889 (+1263 ms) with cwd `…/home-r3/worktrees/repo-r3/handoff-r3-1c8f8a11`. `thread.worktreePath`/`branch` = that worktree. `git worktree list` shows it.
- server.log: 0 `ingestion failed`, 0 `detach-interrupt-failed`, 0 `claude-query-interrupt-timeout`.
- Handoff tool item: `failed`, `toolNonExecutionKind: "user-rejected"` (reproduces the side finding).
- One gap: the committed driver writes no `timing.json` (the capturer's `timeline.mjs` produced it after the fact). I derived the timings above from `parent.events.ndjson`.

## Side finding: handoff tool item `failed`/`user-rejected` on after

- **Mechanism:** `toolNonExecutionKind` comes straight from the Claude SDK's `tool_result_meta[].non_execution_kind` (`ClaudeAdapterV2.ts:2837`). Any `is_error` tool result maps to `failed` (`ClaudeAdapterV2.ts:6283`). The MCP handoff call is still in flight when detach now interrupts the turn, so the SDK labels that tool call as not executed / rejected. R1 got `interrupted` instead, which looks like a race over whether the result or the interrupt lands first.
- **Caused by this diff?** The visible change is, yes: the diff swaps "abandon the session" for "interrupt the turn". The underlying behaviour is pre-existing, though: interrupting any Claude turn during an MCP call gives the same label. Before the fix the item stayed `running` forever, in a thread whose run `failed`, and the continuation never ran. The after state is strictly more truthful at the run level, but still misleading at the item level.
- **Merge blocker for an issue-triage land?** **No.** It is cosmetic, the handoff itself succeeds (worktree created, thread moved, continuation completed), and it beats the pre-fix `running` spinner. File a follow-up: let the handoff's tool result reach the provider before the detach interrupts the turn (or have the handoff tool suppress the in-flight item status), so the transcript does not show a successful handoff as a rejected call.

## Not verified

- Codex controls (R1-codex, R3-codex): the multi-thread branch of `detach` is not exercised at runtime (Codex was rate-limited).
- R4 (archive, runtime-mode change, or metadata update on a running Claude turn without MCP): this is new scope the diff also changes; not captured.
- Draft driver diff: the draft is not preserved. The committed driver reproduces after-R3; the before side was not rebuilt with the committed driver.
