# issue24690 — read-only proof audit, pass 1

**Disposition:** the minimum proof is two protocol fixtures and one matched real-app withdrawal scenario, presented at two web widths. The supplied baseline reproduction is credible; fixed-head real-app proof is still outstanding. This audit did not run tests, start a server, open a browser, capture media, change code, or create a PR.

Audited checkout: `/home/ubuntu/wt/issue-24690`. Full base-to-head diff: [c79ef8c454](https://git.cafe/pingdotgg/t3code/commit/c79ef8c454) → [5b99f0b03f](https://git.cafe/pingdotgg/t3code/commit/5b99f0b03f20cfeab3b968130986f1de7cec449e) (head is local; remote availability is not asserted). Exactly two files changed: ClaudeAdapterV2.ts (+33 production lines) and its existing test file (+120 lines). Checkout was clean; `git diff --check` passed.

## Evidence boundary

- **Read directly:** the full diff, callback branches and artifacts, SDK cancellation implementation, provider-event ingestion, persisted request projection/shell derivation, shared pending-request selectors, web composer/timeline/status consumers, and native mobile consumers. Installed SDK is **0.3.276**; its `control_cancel_request` handler aborts the matching callback controller.
- **Existing evidence read:** `/tmp/issue-24690-repro/runtime-before.json`, `allow.sh`, and `hook.log`. The JSON records a real Chromium web dev run at **1280×800**, base c79ef8c454, hook allowance in `bypassPermissions`, command exit 0, completed turn with stale card, and the `No pending Claude runtime request` response trace. The hook log contains the actual Bash command and bypass permission mode. These are the lead's saved runtime evidence, not a reproduction performed by this auditor.
- **Reported by lead:** actual Claude CLI **2.1.303**, Claude Opus **5.5** through the hub, and **214 tests passing**, including withdrawal and user-cancel cases. The two added test bodies were inspected; the run was not repeated. A green test count does not establish fixed-head client behavior.
- **Pending:** all captures below and fixed-head integrated runtime confirmation. Existing 1280-wide evidence must retain its real dimensions; do not relabel it 1440 or 390.

## Smallest causal fixture matrix

| ID | Real condition / seed | Required result at fixed head | Minimum proof | Status |
|---|---|---|---|---|
| W | Claude approval callback is pending; the PermissionRequest hook answers first and SDK aborts its signal. Use the exact real Bash/full-access scenario below. | Same request becomes `cancelled`, with `resolvedAt`; its approval node and turn item become `cancelled`, with completion timestamps. Pending approval selector drops it. The hook-authorized command may still succeed and the turn may complete: cancelled approval does **not** mean cancelled command. | Added test `cancels a pending approval when Claude withdraws its permission request`, plus actual web evidence C1–C4. Temporal row: **RECORDING** for the runtime transition. | Test body inspected; 214-pass run reported. Baseline runtime evidence exists; head runtime/captures pending. |
| U | A user decision wins first, then SDK aborts. Existing test calls `respondToRuntimeRequest` with `decision: "cancel"` before `controller.abort()`. | Adapter returns `deny`; no withdrawal update replaces the already answered request. Orchestrator resolution remains authoritative (`status: resolved`, `decision: cancel`); node/item may correctly be cancelled. | Added test `keeps a user-cancelled approval resolved when Claude aborts afterwards`. No extra real-app fixture is needed for the minimum: this is a deterministic ordering regression. Any claimed UI exercise of this interaction must use a **RECORDING**, not a still. | Test body inspected; test run reported. No real-app U run claimed. |

The U test verifies adapter non-reemission by reading the next distinct pending request; it does not itself mount the orchestrator or assert a persisted `resolved` row. That distinction should remain explicit. The orchestrator's existing response path persists resolution before dispatching the provider response, which explains the guard. No additional accept/decline/session-option cross-product is justified by this diff.

## Render paths affected, and which need captures

These are consumers of the three changed status events, not new UI implementations.

| Path | Visible/state consequence | Minimum coverage decision |
|---|---|---|
| Web chat → `ChatComposer` → `ComposerPendingApprovalPanel` + `ComposerPendingApprovalActions` | Withdrawn card and its actionable controls leave the composer; full-access explanation leaves with it. Existing command text/reason/options are unchanged. Composer returns to whatever other real state remains. | Primary capture path. C1–C4. Do not add popover/menu-open fixtures just because those controls exist. |
| Web timeline → work group/entry → optional `V2ItemInspector` | Approval history remains. `waiting` maps to `inProgress`; `cancelled` maps to `stopped`. Label may still read “Approval requested”; there is no new mandatory “Cancelled” label. Command output is a separate item. | Include actual history and successful command output in C1/C2; expand the relevant work group once if needed. Do not require a separate inspector screenshot. |
| Web thread shell → sidebar status and thread status pill | `hasPendingApprovals` clears when this was the last pending approval; “Pending Approval” attention must not remain solely for W. | Include the same thread's sidebar/status in desktop recordings. At 390, the closed sidebar is not a missing card fixture. |
| Shared settled/attention derivation → web activity announcer and background notifications | Awaiting-approval state stops being derived from W; normal working/idle/completion state depends on the real run. Existing delivered OS notifications are not promised to retract. | State/transcript inspection in C2, no separate notification or accessibility media fixture. |
| Desktop Electron | Loads the same web components and event stream. No Electron shell/IPC code changed. | Code-traced; separate desktop launch/captures add no distinct changed render branch. Not runtime verified here. |
| Native mobile `use-selected-thread-requests` → `ThreadDetailScreen` → `PendingApprovalCard`; mobile work-log mapping and thread-list status | Same shared pending-status filter unmounts native card; cancelled item maps to `stopped`; list approval attention clears. Native card exit uses its existing animation. | Code-traced only. **390-wide web is not native mobile verification.** No iOS/Android build or capture is part of this minimum server-adapter proof; a claim of verified native interaction would require an actual native recording. |
| Conditional existing consumers: parent/child approval attention banners, mobile MCP app overlay attention, web background-thread announcement | A real affected thread can feed these existing shell/selector consumers. They are conditional on lineage/view state, not extra states created by this patch. | Enumerated reachability only. Do not seed subagents, MCP apps, guest sessions, or extra approvals for this issue. |

Source anchors in the audited checkout: [callback and new terminal events](/home/ubuntu/wt/issue-24690/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:9083), [regression fixtures](/home/ubuntu/wt/issue-24690/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts:7712), [orchestrator resolution](/home/ubuntu/wt/issue-24690/apps/server/src/orchestration-v2/Orchestrator.ts:7840), [shared pending filter](/home/ubuntu/wt/issue-24690/packages/client-runtime/src/state/threadRequests.ts:115), [web card mount](/home/ubuntu/wt/issue-24690/apps/web/src/components/chat/ChatComposer.tsx:3502), [web history status mapping](/home/ubuntu/wt/issue-24690/apps/web/src/session-logic.ts:400), [native card mount](/home/ubuntu/wt/issue-24690/apps/mobile/src/features/threads/ThreadDetailScreen.tsx:1660), [shell pending filter](/home/ubuntu/wt/issue-24690/apps/server/src/orchestration-v2/ProjectionStore.ts:1572).

## Exact real-app seed for W — future authorized primary-agent work

1. Use actual base and fixed-head builds sequentially, preserving each run's commit identity. Stop only a dev server owned by the verifying agent before replacing its state. Do not change a running checkout or point a server at live `~/.t3/userdata`.
2. If a fresh sandbox is needed, run `vp run migrate-dev-db` from the worktree while its server is stopped. This copies a read-only snapshot of `~/.t3/userdata/statev2.sqlite` into `<worktree>/.t3/userdata/statev2.sqlite` and prunes pending work/auth/scheduled runs. Preserve an existing repro sandbox if it already has the needed real history; do not overwrite it merely to satisfy this instruction. Background real-data threads are context, not an invitation to resume them.
3. Reuse the isolated Claude home **`/tmp/issue-24690-repro/claude`**, containing the lead's copied host `env` and PermissionRequest command hook **`/tmp/issue-24690-repro/allow.sh`**. Select/configure a Claude provider instance whose `homePath` points there; the driver supplies `CLAUDE_CONFIG_DIR`. Do not modify the normal host Claude home or export the settings/credentials as evidence. If reconstructing the home, copy only the host settings' `env` privately and the hook below, not all host settings or auth directories.
4. Hook configuration is `hooks.PermissionRequest = [{"hooks":[{"type":"command","command":"/tmp/issue-24690-repro/allow.sh"}]}]`. The existing executable hook reads JSON and returns `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}` only when `.permission_mode == "bypassPermissions"`; it logs its input locally. Verify this condition from the new run's log, without uploading credential-bearing settings.
5. In the real app, add/select the disposable project **`/tmp/issue-24690-repro/project`**. Create a **fresh thread per base/head run** using the actual Claude CLI **2.1.303**, SDK **0.3.276**, and the lead's **Claude Opus 5.5** hub selection. Use **Full access**, ordinary/default interaction mode, with no approval-policy override that changes permission mode. Retain hub routing; no proxy/account changes are needed.
6. Send this exact prompt (the compound command sets up its own disposable victim on every run):

   ```text
   Run this exact Bash command once, then report its output and exit status:
   echo victim > dir.txt; mkdir -p victim; touch victim/a; F=$(cat dir.txt); rm -f $F/*; echo "exit status: $?"
   ```

7. Baseline milestone: hook log says bypass/allow, output says exit status 0, `victim/a` is absent, turn completes, but the approval remains. Record one Approve click and the resulting delivery/protocol failure. On head, let the same natural hook race finish: card is absent after withdrawal/completion, command still succeeds, and no obsolete approval response is offered. Do not click a vanished control or inject a synthetic response.
8. Correlate request ID across real request/node/item terminal events and persisted projection; confirm the shared pending list excludes it. Navigate away/back or reload the completed head thread once to check persisted state. Use event/turn milestones, not an arbitrary sleep, as the end condition. A fast hook may remove the card before it paints; do not add artificial hook delays or claim a visible transient that the recording did not show. The event transcript establishes the pending→cancelled transition in that case.

Do not seed this by hand-writing only a `pending`/`cancelled` projection row: that misses the CLI hook/SDK abort and the original missing-runtime-request defect. Do not reuse the already-stuck baseline request as the fixed-head fixture: this patch handles a live withdrawal, not retrospective cleanup of old stale approvals.

## Exact minimum capture set — planned, none produced in this audit

| ID / suggested filename | Width × chosen height / theme | Format | What must be visible |
|---|---|---|---|
| C1 `w-before-1440-dark.mp4` | **1440×900, dark** | **RECORDING — time + interaction** | Actual base W run: successful output/turn completion with stale approval; one Approve click and the resulting failure. Include thread status/sidebar and relevant work history. |
| C2 `w-after-1440-dark.mp4` | **1440×900, dark** | **RECORDING — time + navigation** | Actual head W run: command succeeds; withdrawal leaves no actionable card or false pending attention. Show history and one away/back or reload persistence check. Pair with request/node/item transcript; do not claim the UI exposes all three statuses. |
| C3 `w-before-390-light.png` | **390×844, light** | Still — terminal-state comparison only | Same actual baseline completed W thread at narrow web width, before the stale Approve click changes its state. Show remaining card, command context, and composer. |
| C4 `w-after-390-light.png` | **390×844, light** | Still — terminal-state comparison only | Same actual head completed W thread at narrow web width; absent card, usable composer, same command context. |

**Total: 2 recordings + 2 stills**, plus a short local, redacted request/node/item/runtime transcript and the existing two focused protocol tests. Obtain C3 before the baseline click in C1, or pause/resume capture deliberately; do not manufacture the baseline state by editing the DOM. C3/C4 do not establish narrow-width timing or interaction. If that is claimed, replace them with recordings rather than quietly claiming stills prove it.

Width/theme rationale: wide and narrow are relevant because removing the composer attachment changes occupied space in both layouts. No stylesheet, theme selection, breakpoint, icon, typography, or light/dark-specific logic changed. Dark at 1440 and light at 390 cover both requested themes without inventing a four-combination visual regression grid; keep each before/after pair in the same theme. No separate 1440-light/390-dark captures are necessary for this protocol fix.

No additional fixtures for AskUserQuestion, ExitPlanMode, resumed/not-resumable approvals, plan/auto/read-only modes, other Claude tool labels, other providers, settings, command palette, remote/tunnel transports, or simultaneous requests: they either return before this changed block or reuse unchanged rendering/transport behavior. Command/file-read/file-change approvals can traverse the generic block, but no kind-specific withdrawal logic changed; Bash W is the representative real fixture. Ordinary user-answer behavior is represented by U. No new reverse action or schema was introduced.

## FRICTION

| Category | Happened | Prevention |
|---|---|---|
| Runtime/protocol | Lead's actual hook won while T3 still displayed a pending approval; Approve then targeted a request already removed from the adapter map. | Correlate real callback abort with terminal request/node/item updates and completion; record the stale-click baseline and the fixed absence. |
| Proof scope | Baseline evidence is 1280×800; fixed-head runtime proof and requested-width media are not supplied. Tests alone cannot establish card disappearance in the real client. | Keep existing evidence dimensions/status honest; collect only C1–C4 after authorization. Label this audit code-traced, not a completed visual verification. |
| Evidence handling | Existing isolated Claude settings contain private environment values. This audit inspected key names/hooks only, not values. | Upload only this sanitized matrix and later redacted media/transcripts; never upload settings.json, `.env`, credentials, or full unrelated histories. |
| Audit tooling | One SDK text search hit minified bundles and produced truncated output; several exploratory filename globs did not exist. | Use discovered paths and bounded substring extraction. The relevant SDK abort handler was subsequently read directly. No code changed. |

Matrix produced with the [ghdrop skill](/home/ubuntu/.codex/skills/ghdrop/SKILL.md); uploaded artifact contains no settings values or authentication material.
