fix(server): close Claude approvals when the CLI withdraws them#24767
Problem
A Claude PermissionRequest hook can answer a permission check while T3 is waiting for the user. Claude then withdraws its request, but T3 leaves the approval pending after the command and turn finish. Clicking Approve fails with No pending Claude runtime request.
Reproduced on Linux with Claude CLI 2.1.303, SDK 0.3.276 and a Full access thread: a PermissionRequest allow hook answers the variable-path rm safety check before the app callback.
Change
Cancel the withdrawn approval request, node and timeline item when the SDK aborts an unanswered callback. Preserve answers already delivered to the adapter. Extend the existing transactional cancellation guard to preserve committed approval answers and feedback while provider delivery is still pending, including separately delivered node and item updates.
Scope and approval
Fixes https://github.com/pingdotgg/t3code/issues/24690
GitCafe: https://git.cafe/pingdotgg/t3code/issues/24690
Triaged bug via the automated issue flow. This fixes withdrawn approval lifecycle state without changing permission policy, contracts or client rendering.
Verification
- Red test on main:
cancels a pending approval when Claude withdraws its permission requestfailed with expectedcancelled, receivedpending. - Final scoped run:
vp test run src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts src/orchestration-v2/ProviderEventIngestor.test.ts— 244 passed. - The committed-answer regression covers accept and decline with feedback, with provider delivery held back and the three cancellation events ingested separately. Both cases fail without the persistence guard and pass with it.
- Server typecheck and scoped lint/format passed. Independent Codex and Claude reviews found no remaining blockers; Codex independently passed 301 related tests.
- Actual base/head web runs: the hook-authorized command exits 0 on both. Before, the completed turn keeps an approval and Approve produces a delivery error. After, request/node/item are cancelled, tool and turn complete, no pending request remains, and reload preserves the absent card.
Independent proof audit: sufficient · Evidence index, exact steps, environment and visible differences · Runtime transcript · Fixture matrix
| Evidence | Before — c79ef8c454 | After — 8625d7af75 |
|---|---|---|
| 1440×900 CSS, dark, DPR 2 | Recording: command succeeds, stale approval, one Approve click and failure | Recording: successful completion and reload without stale approval |
| Terminal view, 1440×900 CSS, dark | Before: successful command with stale approval | After: successful command without withdrawn approval |
| Terminal view, 390×844 CSS, light | Before: stale command approval at narrow web width | After: usable composer with withdrawn approval absent |
Chromium 154 on Ubuntu, default font scaling. The baseline recording has a disclosed idle cut; use the linked unedited version for exact timing because the derivative rebases a sparse variable-frame-rate boundary. Independent real model runs differ in title, response wording, duration and scroll position. Pixel diffs in the index are descriptive, not proof of identical rendering; narrow stability snapshots differ at the blinking caret. No claim that nothing else changed is made.
Native mobile and Electron shell were traced through existing consumers but not launched. Remote/relay/tunnel modes and notifications were not runtime verified. Narrow web is not native mobile verification. The fix handles live withdrawal; it does not retrospectively clean already stale approvals.
Reproduction scripts and sanitized traces are uploaded from the evidence index; none are committed.
Models and harnesses: Claude Opus 5.5 via Claude Code wrote the tests and implementation; GPT-6.1-Sol via Codex led triage, reviewed and captured evidence; Claude Opus 5.5 also reviewed.
Checks
Added 3 commits
Approvability
Verdict: Approved at 8625d7a
Macroscope's review found this PR approvable — This is a localized server bug fix that closes Claude approval state when the provider withdraws it while preserving answers already committed by the user. The transactional guard and focused regression tests cover both withdrawal and answer-delivery races without changing schemas, product defaults, or static-analysis configuration.
You can add or adjust custom eligibility rules. Learn more.
Thread transfer impact
✅ Thread transfer remains within every enforced ceiling.
| Provider | Metric | Main baseline | This PR | Impact | PR ceiling | |
|---|---|---|---|---|---|---|
| Codex | Total thread wire | 5.0 KiB | 5.0 KiB | 0 B (0.0%) | 6.8 KiB | ✅ |
| Codex | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | ✅ |
| Codex | Live turn WebSocket wire | 1.2 KiB | 1.2 KiB | 0 B (0.0%) | 2.0 KiB | ✅ |
| Codex | Live turn WebSocket decoded | 20.9 KiB | 20.9 KiB | 0 B (0.0%) | 29.3 KiB | ✅ |
| Codex | Live turn messages | 2 | 2 | 0 (0.0%) | 8 | ✅ |
| Claude | Total thread wire | 5.0 KiB | 5.0 KiB | 0 B (0.0%) | 6.8 KiB | ✅ |
| Claude | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | ✅ |
| Claude | Live turn WebSocket wire | 1.2 KiB | 1.2 KiB | 0 B (0.0%) | 2.0 KiB | ✅ |
| Claude | Live turn WebSocket decoded | 21.2 KiB | 21.2 KiB | 0 B (0.0%) | 29.3 KiB | ✅ |
| Claude | Live turn messages | 2 | 2 | 0 (0.0%) | 8 | ✅ |
Baseline: c79ef8c · PR result: 8625d7a · Source CI: success
Scenario and decoded snapshot size
10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
- Codex decoded thread snapshot: 108.5 KiB
- Claude decoded thread snapshot: 108.8 KiB
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.
No actionable comments were generated in the recent review.
External image Recent review info
External image Run configuration
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: fix/issue-24690
- File: .coderabbit.config.ts
- Commit: 8625d7a
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
58c19aba-a498-464e-8480-6d3df7924077
External image Commits
Reviewing files that changed from the base of the PR and between c79ef8c454e1268ff79da5a9dcb74572e86f7861 and 8625d7af75e47f140be2bfbb9c3cd462ae6766bc.
External image Files selected for processing: 5
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.tsapps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.tsapps/server/src/orchestration-v2/EventSink.tsapps/server/src/orchestration-v2/ProviderEventIngestor.test.tsapps/server/src/orchestration-v2/ProviderEventIngestor.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
Walkthrough
The Claude adapter now marks withdrawn approvals as cancelled. The event sink and provider ingestor guard against stale cancellation updates replacing resolved approval requests and their terminal states.
Changes: Approval cancellation consistency
Layer / File(s) Summary Claude approval abort handling
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts,apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.tsWhen an abort wins the approval decision race, the adapter marks the approval node, runtime request, and turn item as cancelled. Tests cover withdrawn approvals and approvals already cancelled by a user. Runtime request cancellation guard
apps/server/src/orchestration-v2/EventSink.tsThe event sink broadens its cancellation guard to runtime requests, approval turn items, and linked nodes. Guarded write methods use the expanded guard to filter stale cancellations. Provider cancellation ingestion
apps/server/src/orchestration-v2/ProviderEventIngestor.ts,apps/server/src/orchestration-v2/ProviderEventIngestor.test.tsProvider ingestion enables the runtime-request cancellation guard on its write paths. Regression tests check that provider cancellation preserves resolved approval decisions and their terminal states. Priority: External image Normal
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Bug fix · Severity of issue fixed: Medium
Possibly related PRs
- pingdotgg/t3code#22023: Withdraws pending approvals when a provider turn stops and adds guards that preserve committed approval answers.
Suggested labels:
bugfix,size:LSuggested reviewers:
scottn-pv,t3dotgg,macroscopeapp
Merge Risk: External image Minimal · up to 8625d
Withdrawn approvals can close without replacing answers already given. No actionable issue remains before merge beyond normal checks.
Pre-merge checks: Passed 4
Check name Status Explanation Title check Passed The title clearly and concisely describes the main change: closing Claude approval requests when the CLI withdraws them. Description check Passed The description includes complete Problem, Change, Scope and approval, and Verification sections. It identifies the issue, explains the implementation, documents focused tests and observed results, an… Linked Issues check Passed Issue #24690requires withdrawn unanswered approvals to settle the request, node, and approval item as cancelled, while preserving a user answer.ClaudeAdapterV2.tsnow claims the unresolved decisio…Out of Scope Changes check Passed The changes stay within issue #24690. TheEventSink.tsoption rename and guard expansion support approval cancellation and answer preservation. The adapter changes update only withdrawn approval lif…Full details: Description check
Explanation
The description includes complete Problem, Change, Scope and approval, and Verification sections. It identifies the issue, explains the implementation, documents focused tests and observed results, and states unverified environments and limitations.
Full details: Linked Issues check
Explanation
Issue
#24690requires withdrawn unanswered approvals to settle the request, node, and approval item as cancelled, while preserving a user answer.ClaudeAdapterV2.tsnow claims the unresolved decision only when the callback signal is aborted and emits cancelled updates for all three records.EventSink.tsnow guards runtime-request cancellations, including approval items and separately delivered node/item updates. The added tests cover withdrawal and committed accept/decline answers with feedback. The implementation also preserves the answer when provider delivery withdraws the request.
Full details: Out of Scope Changes check
Explanation
The changes stay within issue
#24690. TheEventSink.tsoption rename and guard expansion support approval cancellation and answer preservation. The adapter changes update only withdrawn approval lifecycle state. The tests cover the reported regression and its race condition. No unrelated product behavior or client rendering changes are shown.
Finishing Touches
📝 Generate docstrings
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
- Commit to this branch
- Create a new PR
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
Note
🤖 Claude Opus 5.5 responding on behalf of Lars Wolff (larswolff)
Claude's native plan mode also triggers #24690, on ordinary tool calls rather than only a bypass-immune safety check. One plan-mode turn left 10 stale approval cards.
Setup: T3 Code 0.1.3-nightly.20261027.3754, Claude Code 2.1.311, Linux, Full access thread, and a PermissionRequest hook that allows when permission_mode is plan.
- Claude called
EnterPlanMode. T3's interaction mode stayed Default. - Ten calls then reached
canUseTool: a Bashprintfwith$(...), acpto/tmp, and 8delegate_taskcalls (their results carryreasonCode: "mode:plan"). - For each, the transcript shows the hook allowing 7–30 ms after the
tool_use, then a result withpermissionDecision.source: "hook". The Bash commands ran and the delegate tasks started. - All 10 cards stayed open. Answering them later gave 10 "Answer delivery not confirmed" errors, and the server log shows
No pending Claude runtime request.
When the abort signal wins, main at 10e709c deletes the pendingRuntimeRequests entry but emits no terminal update for the request, node or approval item (ClaudeAdapterV2.ts#L9541-L9552).
From reading the diffs (not run): all three open PRs fix that path. Only this one also keeps a withdrawal from overwriting an answer that is committed but not yet delivered to the adapter. Could it get a review?