fix(server): close Claude approvals when the CLI withdraws them#24767

open3
main
fix/issue-24690
opened3d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 31m ago

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 request failed with expected cancelled, received pending.
  • 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

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

22 successful · 7 skipped
All checks passed
8625d7a

Added 3 commits

  • fix(server): a Claude approval withdrawn by a hook no longer stays pending (#24690)5b99f0b
  • test(server): reproduce withdrawn Claude approval staying pending (#24690)84a2ff6
  • fix(server): a withdrawn approval no longer erases a committed answer (#24690)8625d7a
Synced from GitHub

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.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
CodexLive turn WebSocket decoded20.9 KiB20.9 KiB0 B (0.0%)29.3 KiB✅
CodexLive turn messages220 (0.0%)8✅
ClaudeTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.2 KiB21.2 KiB0 B (0.0%)29.3 KiB✅
ClaudeLive turn messages220 (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.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
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.ts
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
  • apps/server/src/orchestration-v2/EventSink.ts
  • apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts
  • apps/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.ts
When 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.ts
The 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.ts
Provider 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:L

Suggested 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 nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main change: closing Claude approval requests when the CLI withdraws them.
Description checkPassedThe 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 checkPassedIssue #24690 requires withdrawn unanswered approvals to settle the request, node, and approval item as cancelled, while preserving a user answer. ClaudeAdapterV2.ts now claims the unresolved decisio…
Out of Scope Changes checkPassedThe changes stay within issue #24690. The EventSink.ts option 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 #24690 requires withdrawn unanswered approvals to settle the request, node, and approval item as cancelled, while preserving a user answer. ClaudeAdapterV2.ts now claims the unresolved decision only when the callback signal is aborted and emits cancelled updates for all three records. EventSink.ts now 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. The EventSink.ts option 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.

Synced from GitHub

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.

  1. Claude called EnterPlanMode. T3's interaction mode stayed Default.
  2. Ten calls then reached canUseTool: a Bash printf with $(...), a cp to /tmp, and 8 delegate_task calls (their results carry reasonCode: "mode:plan").
  3. For each, the transcript shows the hook allowing 7–30 ms after the tool_use, then a result with permissionDecision.source: "hook". The Bash commands ran and the delegate tasks started.
  4. 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?