Skip to content

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

Open
juliusmarminge wants to merge 7 commits into
mainfrom
fix/issue-24690
Open

juliusmarminge wants to merge 7 commits into
mainfrom
fix/issue-24690

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

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 #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

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.

juliusmarminge and others added 3 commits October 25, 2026 06:10
…4690)

A Claude PermissionRequest hook can answer before the user does; the CLI
then cancels the can_use_tool control request and the SDK aborts the
callback signal. The adapter resolves the callback but never emits a
terminal runtime request update, so the approval card stays pending.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nding (#24690)

When a PermissionRequest hook answers first, the CLI cancels the
can_use_tool request and the SDK aborts the callback signal. The adapter
resolved the callback but left the request, node and turn item pending,
so the card stayed open and answering it failed with "No pending Claude
runtime request". The abort path now claims the decision and emits the
same cancelled shape the ingestor uses for dismissed native requests. A
user answer completes the decision first, so it keeps its resolution.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…#24690)

An approval answer commits before its outbox delivery reaches the provider.
If Claude withdraws the request in that gap, the adapter's cancellation
events overwrote the resolved request, losing the decision and feedback and
flipping the node and item outcome. The EventSink cancellation guard now
covers approvals too, and checks node and item cancellations against their
request, since the adapter writes each in its own batch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 25, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 25, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 7589a23

Macroscope's review found this PR approvable — The change protects committed approval answers from stale provider cancellations, including separately delivered node and timeline updates. Its scope is request lifecycle bookkeeping, without changing permission policy or product defaults.

No code changes detected at 6259c4e. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

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 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 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.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Other scenarios

All 64 scenario measurements match main.

All 64 scenario measurements
Provider Scenario Metric Main baseline This PR Impact PR ceiling
Codex short turn resume WebSocket decoded 10.5 KiB 10.5 KiB 0 B (0.0%) 12.7 KiB ✅
Codex short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn running, cold open HTTP decoded 281.7 KiB 281.7 KiB 0 B (0.0%) 302.7 KiB ✅
Codex long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Codex long turn running, history page HTTP decoded 92.4 KiB 92.4 KiB 0 B (0.0%) 107.4 KiB ✅
Codex long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Codex long turn finished, cold open HTTP decoded 381.5 KiB 381.5 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, socket cold open WebSocket decoded 381.6 KiB 381.6 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, work page HTTP decoded 186.2 KiB 186.2 KiB 0 B (0.0%) 200.2 KiB ✅
Codex long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Codex idle thread resume WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell snapshot HTTP decoded 42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅
Codex fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Codex fleet shell subscribe WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Codex fleet shell resume check rows 0 0 0 0 ✅
Codex stale shell reload HTTP decoded 42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅
Codex stale shell reload rows 27 27 0 (0.0%) 27 ✅
Codex stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Codex stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex stale todo list cold open HTTP decoded 72.4 KiB 72.4 KiB 0 B (0.0%) 83.0 KiB ✅
Codex stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Codex stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅
Claude short turn resume WebSocket decoded 10.6 KiB 10.6 KiB 0 B (0.0%) 12.7 KiB ✅
Claude short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn running, cold open HTTP decoded 283.3 KiB 283.3 KiB 0 B (0.0%) 302.7 KiB ✅
Claude long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Claude long turn running, history page HTTP decoded 93.4 KiB 93.4 KiB 0 B (0.0%) 107.4 KiB ✅
Claude long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Claude long turn finished, cold open HTTP decoded 384.2 KiB 384.2 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, socket cold open WebSocket decoded 384.3 KiB 384.3 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, work page HTTP decoded 188.1 KiB 188.1 KiB 0 B (0.0%) 200.2 KiB ✅
Claude long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Claude idle thread resume WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell snapshot HTTP decoded 43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅
Claude fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Claude fleet shell subscribe WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Claude fleet shell resume check rows 0 0 0 0 ✅
Claude stale shell reload HTTP decoded 43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅
Claude stale shell reload rows 27 27 0 (0.0%) 27 ✅
Claude stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Claude stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude stale todo list cold open HTTP decoded 73.4 KiB 73.4 KiB 0 B (0.0%) 83.0 KiB ✅
Claude stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Claude stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅

Baseline: 5f85768 · PR result: 6259c4e · 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.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The event sink now filters stale runtime-request cancellations, including approval cancellations, across guarded write paths. Provider event ingestion enables this guard. Regression tests check that unanswered approvals are cancelled and committed answers remain unchanged.

Changes: Approval cancellation consistency

Layer / File(s) Summary
Runtime-request cancellation guard
apps/server/src/orchestration-v2/EventSink.ts
The write methods use guardPendingRuntimeRequestCancellations. The guard filters stale cancellation events for resolved requests, linked nodes, and user-input or approval turn items.
Provider cancellation ingestion
apps/server/src/orchestration-v2/ProviderEventIngestor.ts, apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts
Provider ingestion enables the runtime-request guard on its write paths. Tests check cancellation of unanswered approvals and preservation of committed accept or decline outcomes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related PRs

  • pingdotgg/t3code#27520 — Both changes define how approval cancellation treats an already saved answer; this PR also adds a Copilot SDK adapter.
  • pingdotgg/t3code#30922 — Both changes extend transactional cancellation checks to approvals to preserve a response committed before cancellation.
  • pingdotgg/t3code#22023 — Both changes address cancelling approvals while preserving a concurrent answer.

Suggested reviewers: scottn-pv, t3dotgg, yash-singh1

Merge Risk: Minimal · up to 6259c

The change stops a late Claude withdrawal from overwriting an approval answer that is already committed. No concrete merge-blocking risk was identified in the supplied changes, and the added tests cover the key cases.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: Claude approvals are closed when the Claude CLI withdraws them. The raw summary covers a guard rename and a test, which fit under this fix. The title uses the conventi…
Description check Passed The description follows the template. It has Problem, Change, Scope, and Verification sections with substantive content. The Problem section gives the reproduction steps and environment. The Change se…
Linked Issues check Passed Issue #24690 is closed and completed. The instructions treat it as background only, so no active directly linked coding requirements apply. The head-versus-base diff of ClaudeAdapterV2.ts is empty, so…
Out of Scope Changes check Passed Changes since the prior review and the whole-PR diff touch EventSink.ts, ProviderEventIngestor.ts and their tests. In EventSink.ts, the option guardPendingUserInputCancellations is renamed to guardPen…
Full details: Title check

Explanation

The title names the main change: Claude approvals are closed when the Claude CLI withdraws them. The raw summary covers a guard rename and a test, which fit under this fix. The title uses the conventional commit format the template asks for.

Full details: Description check

Explanation

The description follows the template. It has Problem, Change, Scope, and Verification sections with substantive content. The Problem section gives the reproduction steps and environment. The Change section explains the fix. The Scope section links issue #24690. The Verification section lists tests and describes the observed before and after results. The template asks for a conventional commit title, and the title meets that. The description is long and includes many evidence links, but it covers every required section.

Full details: Linked Issues check

Explanation

Issue #24690 is closed and completed. The instructions treat it as background only, so no active directly linked coding requirements apply. The head-versus-base diff of ClaudeAdapterV2.ts is empty, so the adapter-side withdrawal cancellation described in the PR description is not in this head. That absence does not fail this check, because the closed issue does not require it. The author's description and the objectives still describe that work; a reviewer should confirm whether it was intended for this PR.

Full details: Out of Scope Changes check

Explanation

Changes since the prior review and the whole-PR diff touch EventSink.ts, ProviderEventIngestor.ts and their tests. In EventSink.ts, the option guardPendingUserInputCancellations is renamed to guardPendingRuntimeRequestCancellations. The guard now also checks node.updated, turn-item.updated (user_input_request and approval_request) and runtime-request.updated (approval kind). It drops such cancellations when the stored request is already resolved. ProviderEventIngestor.ts enables the renamed guard in its three write paths. The regression test covers committed approval answers and withdrawal of an unprojected approval. All of these changes concern approval lifecycle and answer preservation, which the PR description states as its purpose. No unrelated product behavior appears in the reviewed changes. The raw summary is partial, so this verdict covers only the listed files and the empty adapter diff.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 7589a23 and 6259c4e.

Files selected for processing: 3

  • apps/server/src/orchestration-v2/EventSink.ts
  • apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts
  • apps/server/src/orchestration-v2/ProviderEventIngestor.ts

Limit details: You’ve used all 10 included reviews currently available.

@larswolff

Copy link
Copy Markdown

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?

Copy link
Copy Markdown
Member Author

Thanks for the thorough fix and evidence. #27320 already handles withdrawn Claude approval cards on main in 41588d4520. Please cut this PR down to the separate transactional guard that preserves committed answers when withdrawal races provider delivery, with its focused regression coverage.

# Conflicts:
#	apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
A provider can withdraw an approval it never surfaced to projection; drop
its cancellation only when the request is already resolved.

AI-assisted; verified with the tests and tools listed above.

Copy link
Copy Markdown
Member Author

The automated pipeline stopped on this pull request and will not merge it:

someone else pushed to fix/issue-24690 (head is now 8625d7a); the pipeline does not build on commits it did not review

It stays open for a person to finish or close.

AI-assisted; verified with the tests and tools listed above.
@macroscopeapp
macroscopeapp Bot dismissed their stale review November 11, 2026 05:59

Dismissing prior approval to re-evaluate 7589a23

@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Nov 11, 2026
@juliusmarminge

Copy link
Copy Markdown
Member Author

The automated pipeline stopped on this pull request and will not merge it:

the capture step could not show the problem on the base commit, so there is nothing to prove fixed.

On each commit, configured a project-local Bash PermissionRequest hook to return allow after 15 seconds. Opened a Full access Claude thread and requested exactly: echo victim > dir.txt; mkdir -p victim; touch victim/a; F=$(cat dir.txt); rm -f $F/*. Both runs denied the command before execution without displaying an approval card. Read-only inspection confirmed zero projected runtime requests on both sides. No answer could be submitted, so the required answer-commit/withdrawal race was not reached.

It stays open for a person to finish or close.

@juliusmarminge juliusmarminge added the takeover:in-progress A takeover lead is working on this PR label Nov 13, 2026
@juliusmarminge juliusmarminge added takeover:blocked Takeover blocked; reason in the PR body and removed takeover:in-progress A takeover lead is working on this PR labels Nov 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:needs-julius Needs Julius: route gate, unverifiable surface, or blocked size:M 30-99 changed lines (additions + deletions). takeover:blocked Takeover blocked; reason in the PR body vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude approval stays pending after the CLI withdraws its request (PermissionRequest hook, Auto mode)

2 participants