# fix\(server\): withdraw aborted Claude questions · PR \#22803 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22803)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: fix/issue-15517

Target branch: main

Head commit: d1293779b0fe6a5127b73d7b7cf096674d35a128

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22803)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22803/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22803/diffs?format=markdown)

## Description

---

## Problem

Steering while Claude waits on AskUserQuestion leaves the aborted question pending. Answering its stale card fails delivery with `No pending Claude runtime request`, while a replacement question waits. Fixes [#15517](https://github.com/pingdotgg/t3code/issues/15517).

## Change

The adapter now emits cancellation for the aborted question's request, question node and turn item. Successful answers retain their existing payload. Approval requests and modes are unchanged.

## Scope and approval

Fixes #15517
GitCafe: https://git.cafe/pingdotgg/t3code/issues/15517
Triaged bug via the automated issue flow.

Validation: 204 adapter tests pass; the added regression fails with only the implementation reverted. Scoped formatting and server typecheck pass; lint has one pre-existing unused `layer` warning. Independent Codex GPT-6.1-Sol and Claude Opus 5.5 reviewers found no confirmed defects (both MERGE). Proof audit pass 2: SUFFICIENT for F1–F4; independent adapter harness 204 passed. Timing races remain unverified.

## Verification

> **Superseded:** the evidence in this section was captured at [b0e0163e80](https://git.cafe/pingdotgg/t3code/commit/b0e0163e805da78f5156ca283cc35f3367a6df9f), before main was merged in. See `## Evidence` below for the server-surface evidence captured at [2fb0145392](https://git.cafe/pingdotgg/t3code/commit/2fb01453927cad79c5b7d54e74764500c2e54488).

Before [e27a03e94d](https://git.cafe/pingdotgg/t3code/commit/e27a03e94d43b504cfc3933282af913763312afd); after [b0e0163e80](https://git.cafe/pingdotgg/t3code/commit/b0e0163e805da78f5156ca283cc35f3367a6df9f).

[Narrated captures index](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md) · [Original matrix](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/63924d1b41f8fd38/pr-22803-matrix.md) · [Claude Opus 5.5 re-audit](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e43dc65483e26bf2/pr-22803-proof-reaudit.md): **INSUFFICIENT R3 R4**.

Eight before/after recording pairs use the same local Claude SDK protocol stub in real web threads. Each starts with a pending Red/Blue question. Stop and process-failure scripts withdraw it, then submit a fresh prompt and answer Blue on both revisions. Steering sends “Replace the question” through authenticated `message.dispatch`, cancels Q1, gates Q2, then clicks Blue. Before retains stale Q1; after withdraws it and completes Q2. Script logs and decoded playback checkpoints appear above every pair.

Decisive desktop-light pair: after has no question at 4.1 s, replacement Q2 at 8.2 s and Blue acknowledgement at 12.2 s; before still has Q1 at 4.0/8.2 s and an unanswered replacement at 12.2 s.

| Before | After |
|---|---|
| [R5-before.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e1ade30d8a03086c/R5-before.mp4) | [R5-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/59adbc3267a14b43/R5-after.mp4) |

| Row | Interaction | Viewport / theme | Coverage |
|---|---|---|---|
| [R1](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r1) | Stop button | 1440×900 light | Recording pair |
| [R2](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r2) | Stop button | 1440×900 dark | Recording pair |
| [R3](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r3) | Stop command via RPC | 390×844 light | UI button unverified; harness pair |
| [R4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r4) | Stop command via RPC | 390×844 dark | UI button unverified; harness pair |
| [R5](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r5) | New-message steering via RPC | 1440×900 light | Decisive recording pair |
| [R6](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r6) | New-message steering via RPC | 390×844 dark | Decisive recording pair |
| [R7](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r7) | Provider process failure | 1440×900 dark | Recording pair |
| [R8](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#r8) | Provider process failure | 390×844 light | Recording pair |
| [S9](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8dc90715a627acc/captures-index.md#s9) | Pending card | 1440×900 light | Still pair + two diff images |

**Unverified:** R3/R4 actual Stop-button clicks: the pending-question layout renders no Stop control at 390 px on either revision. RPC cancellation/recovery is recorded and labelled; it does not satisfy those original matrix rows. R5/R6 use the matrix’s allowed RPC path because the local composer is answering the question. No matrix waiver or UI change was made. An unbound, configurable `thread.stop` shortcut exists; it was not configured or captured.

**Out of scope:** remaining steering/failure viewport-theme combinations (eight-recording cap); native mobile/Electron shell; remote/tunnel paths; other providers or permission prompts; real Claude subscription/binary and additional timing races (deterministic SDK protocol fixture used).

**Environment:** fs-10 hostname, machine marker `freestyle`; Linux x86_64, Chromium 154 desktop UA, DPR2; CSS 1440×900 / 390×844, videos 2880×1800 / 780×1688, light/dark. Independent worktree-local `.t3` state, synthetic project, no real conversations seeded. Still tool downsamples the full 1440×900 surface to 1280×800.

**Static pending card — before:**

![S9-before.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9a02417090c3750e/S9-before.png)

**Static pending card — after:**

![S9-after.png](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/24bfb14f770e874a/S9-after.png)

Both diff images are in S9. Independent file-level rebuild: 385/1,024,000 changed pixels (0.0376%), entirely the “Input requested” shimmer phase; no clock/title difference. The auditor did not perform a live re-capture.

What else changed: no web styling or contract change in the PR. Independent IDs, ports, message times, title timing and elapsed durations vary between captures; viewport and theme changes are intentional. Stop/failure already clean up at the base revision; steering is the distinguishing proof.

Focused adapter regression: 1 passed, 203 filtered/skipped — [focused-test.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/258d360f4ba3fd7f/focused-test.txt). Runtime/protocol evidence: [runtime-evidence.tar.gz](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/988d2aa653ff4c3e/runtime-evidence.tar.gz).

Capture: Codex gpt-6.1-sol; proof audit: Claude Opus 5.5

Evidence re-captured as narrated recordings per EVIDENCE.md.

Model: GPT-6.1-Sol through the Codex harness in T3 Code; delegated test, implementation, review, and proof-audit agents.

## Evidence

Server surface: scoped tests plus runtime transcripts from a real `apps/server` process. **Before** = main [8d3b061121](https://git.cafe/pingdotgg/t3code/commit/8d3b061121fe65669d5d43d011b48cd3027a4108). **After** = [2fb0145392](https://git.cafe/pingdotgg/t3code/commit/2fb01453927cad79c5b7d54e74764500c2e54488), which is the main merge [e7f57a52be](https://git.cafe/pingdotgg/t3code/commit/e7f57a52be493928cfd0b79983a772b7c53011ef) plus the committed fixture and driver. The current head [d1293779b0](https://git.cafe/pingdotgg/t3code/commit/d1293779b0fe6a5127b73d7b7cf096674d35a128) adds only a `knip.jsonc` entry. Both revisions ran the same script, `apps/server/scripts/claude-question-withdrawal-evidence.ts`, against the same seed, each with its own isolated home. Every row is labelled **fixture: `claudeQuestionMockCli`** (`apps/server/src/provider/testFixtures/claudeQuestionMockCli.mjs`). That stub speaks the Claude Agent SDK's stream-json control protocol and is wired in through `providerInstances.claudeAgent.config.binaryPath`. The SDK, adapter, orchestrator, projections and WebSocket are all real. Each row waited until the client stream showed Q1 as `pending` before acting. Persisted events come from `orchestration_events`. Client data comes from `orchestration.subscribeThread`.

Matrix: [matrix-pass1.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e51b264ec3e14757/matrix-pass1.md) · Captures: [captures-index.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3c1c329953c7d14d/captures-index.md) · Raw: [pr22803-raw-evidence.tar.gz](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3e80d6d9d6e2a7b0/pr22803-raw-evidence.tar.gz)

| Row | Trigger | Before (persisted + client) | After (persisted + client) | Discriminating |
|---|---|---|---|---|
| A1 | run.interrupt while Q1 pending | Q1 pending→cancelled in the terminal batch; run interrupted ([A1-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f9ea958ace28b242/pr22803-A1-before.txt)) | Q1 cancelled by the adapter before the turn ended; run interrupted; one cancel ([A1-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/46800796d9e43920/pr22803-A1-after.txt)) | No (same end state) |
| A2 | thread.stop | pending→cancelled at the terminal timestamp; interrupted ([A2-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9ce3a5c60568896c/pr22803-A2-before.txt)) | adapter cancel 16 ms before the terminal event; interrupted; one cancel ([A2-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f39f8fbe3bbcd075/pr22803-A2-after.txt)) | No |
| A3 | provider process exits 1 mid-question | pending→cancelled with provider-turn failed ([A3-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1a0e3a003bde7e8d/pr22803-A3-before.txt)) | adapter cancel 23 ms before failed; one cancel ([A3-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/574e851cb5a74488/pr22803-A3-after.txt)) | No |
| A4 | provider control_cancel_request, turn alive, replacement Q2 | **at Q2 pending: stream and DB both show 2 pending (Q1, Q2)**; Q1 cancelled only when the turn completed ([A4-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1e43858aa7721b1a/pr22803-A4-before.txt)) | **at Q2 pending: stream and DB show Q2 only**; Q1 cancelled@160 before Q2 pending@166; no duplicate ([A4-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/262e7ff0eee7a3ae/pr22803-A4-after.txt)) | **Yes** |
| B | runtime-request.respond Blue | pending→resolved, 0 cancelled ([B-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/abd7135fd437e86d/pr22803-B-before.txt)) | pending→resolved, 0 cancelled ([B-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/40856fcf99a791ff/pr22803-B-after.txt)) | No (unchanged) |
| C | stale control_cancel_request after the answer | pending→resolved, 0 cancelled ([C-before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d58b20c82710d85d/pr22803-C-before.txt)) | pending→resolved, 0 cancelled ([C-after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/57497425901f301b/pr22803-C-after.txt)) | No (no spurious cancel) |

A4 at the moment Q2 is pending (client stream):
```
BEFORE  seq=153 runtime-request pending toolu_q1 → cancel sent → seq=161 pending toolu_q2 → K: pending=["toolu_q1","toolu_q2"]
AFTER   seq=156 runtime-request pending toolu_q1 → cancel sent → seq=160 cancelled toolu_q1 (+node, turn-item cancelled) → seq=166 pending toolu_q2 → K: pending=["toolu_q2"]
```

**Environment:** fs-12, Linux 6.1.102 x86_64, Node v26.12.1, `@anthropic-ai/claude-agent-sdk` 0.3.276. Stub sha256 `d488d671…6b0067`, driver sha256 `eff31b46…dce34c`, byte-identical in both trees.

**What else changed:** IDs, sequence numbers, ports, PIDs and timestamps differ between runs. In A1–A3, BEFORE's cancel comes from the ingestor's terminal-batch dismissal, while AFTER's comes from the adapter 15–25 ms earlier. If the turn end were processed first, AFTER would write a second, cosmetic `cancelled` update. That order was never seen in about 8 AFTER runs, and it is not ruled out. The driver waits a fixed 1.5 s after the run ends to catch late events.

Rebuild one row: `node apps/server/scripts/claude-question-withdrawal-evidence.ts --worktree "$PWD" --home /tmp/ev/after/home --out /tmp/ev/after --row A4`

<details><summary>Verification log</summary>

- Proof audit pass 1 (Claude Opus 5.5): derived the server matrix above and replaced the earlier UI rows R3/R4, which were found INSUFFICIENT, with server-side interrupt and stop rows. A4 is the discriminating row.
- Proof audit pass 2 (Claude Opus 5.5): **SUFFICIENT** for A1, A2, A3, A4, B and C. SHAs and fixture hashes match. It rebuilt A4 from the committed script against 2fb0145392 in a fresh home and got the same result once IDs were normalised. [audit-pass2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/992aa0685d73443c/audit-pass2.md)
- Tests: `ClaudeAdapterV2.test.ts` 208/208 on the merged head, run by both the lead and the auditor. Server `tsc --noEmit` clean, suggestions only. Targeted `vp check` on the four changed files: 0 errors, plus the pre-existing unused `layer` warning. `knip:check` passes after d1293779b0. Before that commit it flagged the new driver, which was the likely CI Lint failure.
- CI: when this was written, the reported checks were Lint and Check failing and CodeRabbit pending, on a head before d1293779b0. CI for d1293779b0 is pending.
- Unverified: no real Claude turn (the fixture is deterministic, and a real model cannot produce A4 on demand); no steer-only variant; no UI rendering (the surface is server only); the duplicate-cancel order in A1–A3.
- Model/harness: Claude Opus 5.5 through Claude Code in T3 Code (evidence lead, audits and capture as delegated Claude Opus 5.5 tasks).

</details>

