Repository navigation
fix(approval): present parked approvals and settle the turns they end - #1361
Merged
TheGreatAxios merged 1 commit intoOct 8, 2026
Conversation
TheGreatAxios
force-pushed
the
cl-9713-add-grant-preserving-watchdog-recovery-for-stuck-agent-calls
branch
from
October 8, 2026 23:20
5514c68 to
2417791
Compare
TheGreatAxios
marked this pull request as ready for review
October 8, 2026 23:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
deliver()(a worker wake, a background shell exit, a queued prompt on an idle agent) reaches the operator. Onlysend()turns a park into asuspendedresult, so these approvals used to sit unseen until they timed out. The stream sink now routes approvalreactor.gate.blockedevents through the same correlated approval path a send uses. Whichever of the two arrives first presents the approval and the other joins it, so the operator is asked once.tool.done, so the TUI kept it active and never settled the turn. The bridge records which active calls had not started executing when an approval gate blocked. The reactor cannot re-infer while a call is parked, so aninference.startthat finds one still unstarted closes it as "not run" through the same path atool.donetakes. Approved re-dispatches, siblings already executing, and non-approval gates are left alone.Why the Ctrl+C change is here
This is an existing gap that the first two fixes expose, not a regression. A permission or operator prompt has always held every key until it was answered (
src/tui/shell/keys.tsreturns from the overlay branch before it reaches the Ctrl+C handler), and that is the same onmain. The only change to key handling in this PR is the last commit.What this PR changes is how often it matters. A turn that continues after an approval is decided is not a send, so on
mainits further approvals park silently instead of prompting. With every parked approval now presented, a run that keeps issuing tool calls after a rejection can raise prompts faster than they can be answered: each rejection re-infers into more calls, often several at once, and every one needs approval under--no-auto. In a live run on this branch before the Ctrl+C change that was 21 prompts in about 100 seconds, with Ctrl+C, typingexitand/modelall swallowed until the session was killed. That sequence was not run onmain. Shipping the first two fixes without the Ctrl+C change would leave it reachable, so it is included here. An explicit "Reject and stop" choice in the prompt is a separate, lower-priority request.Verification
bun run checkpasses (lint, typecheck, dead exports, build, guarded test suite: 8380 pass, 0 fail)bun run typecheckpasses at each of the six commitse2e/reactor-approval-bare-deliver.test.tsdrives the real reactor with a baredeliver()and pins the silent park, the stream route, and the watchdog re-presenting the persisted approvale2e/reactor-approval-parked-call-settles.test.tscaptures the real reactor's stream for a rejected and an approved parked call, confirms the rejected one emits notool.done, and replays both into the bridge: the rejected call closes as not run and the turn settles, and the approved one reports its own result. The rejected case fails without the bridge change.--no-auto): a Reject in a normal turn and a Reject in a turn started by a background shell exit both closed the call as not run and settled the turn without an interrupt. An approval in a turn started by a background shell exit was raised as soon as the turn parked, and the accepted call ran. Onmain, the same Reject leaves the turn at "working".tool.done), which the tests above cover.Manual test
Auto mode is on by default and auto-approves non-destructive shell commands, so pass
--no-autoor nothing parks. Use a model with well-formed tool calls (codex/plus · gpt-5.6-terra intermittently sent garbled working-directory arguments, which the gate hard-denies before any approval).bun install && bun run build:bin, then./dist/corbits --no-auto. Confirm the header line shows noyolo.touch cl9713-smoke-sendas its own command in the current directory." Reject it. The row readsnot run: no approval was grantedand the footer returns to idle.sleep 15. Run nothing else now and end your turn. When you are notified the background command finished, runtouch cl9713-smoke-deliveras its own command in the current directory." Accept thesleeponce. About 15 seconds later thetouchprompt appears on its own. Accept once; the call runs and the turn settles.touch. The turn settles as in step 2.ls cl9713-smoke-*shows onlycl9713-smoke-deliver. Remove it.Fixes CL-9713
🤖 Generated with Claude Code