Fold gate blocked-ness into turn state so the stall watchdog sees it - #381
Merged
TheGreatAxios merged 4 commits intoAug 8, 2026
Conversation
The painter derived a local "blocked" turn from shell.overlayKind and the stall check read the shared bag.turn straight, so the exemption never reached the watchdog: an operator reading an approval for the duration of the stall timeout got the run aborted underneath them. Gates still queued behind another overlay were worse off, since overlayKind reflects whatever else is on screen. Turn state now carries a blocked-gate count, incremented the moment a gate is raised (queued or displayed alike) and decremented when it resolves, driven directly off the gate-wire lifecycle rather than the shell's overlay. The painter and the watchdog both read that one field instead of each re-deriving blocked-ness on their own.
onPermission and onOperator each rewrapped resolve with the same closed-once bookkeeping; onceClosed makes that a single helper both gate handlers share.
Interrupting or settling a turn does not close the permission/operator overlay still on screen, so a gate raised in one turn can still be sitting open when the next turn starts, or after the current one has already settled. Resetting blockedGateCount to zero on every turn transition lost track of that gate: its eventual close would land against whatever turn happened to be live by then, either dropping that turn's own exemption early or silently reviving a settled turn's status to "running". Turn resets now carry the prior count forward instead of dropping it, and only mark a fresh turn "blocked" rather than "running" when a carried-over gate is still open. A terminal reset (done, stopped, failed) is left with its terminal status intact, since every one of those already satisfies the watchdog's exemption on its own.
Both are independent guards feeding the same silentPastThreshold check in the stall watchdog. Add pure and bridge-level tests asserting each exempts alone and that neither's guard accidentally requires the other's condition to also hold.
TheGreatAxios
force-pushed
the
cl-5642-gate-blocked-turns-are-exempted-from-the-stall-watchdog-only
branch
from
August 8, 2026 16:47
91495fa to
b3f818c
Compare
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
blockedGateCount, incremented the moment a permission/operator gate is raised (queued or already displayed) and decremented when it resolves, driven off the gate-wire lifecycle rather thanshell.overlayKind.paintPhaseand the stall watchdog now both readbag.turn.statusdirectly — the local, paint-only "blocked" derivation is gone, so the exemption reaches the watchdog instead of stopping at the painter.wireGatesaccepts optionalGateLifecycleHooks(onGateOpened/onGateClosed), wired inproduct-host.tstobridge.gateOpened()/bridge.gateClosed()."running". Terminal statuses (done/stopped/failed) are left alone when carrying the count forward, since they already satisfy the watchdog'sstatus !== "running"exemption on their own.resolvewrapper shared byonPermission/onOperatoringate-wire.tsinto a singleonceClosedhelper.Rebase onto main (CL-5641 + CL-5644 merged first)
Rebased onto current
mainafter PR #383 (CL-5641, threadsactiveToolCallsintoShouldAbortForStallArgs) and PR #382 (CL-5644, transcript echo suppression) landed. Both are independent exemptions feeding the samesilentPastThresholdcheck install-watchdog.ts:args.status !== "running"(this PR's gate exemption) is checked beforeactiveToolCalls(CL-5641's exemption), so either one alone — or both together — keeps the watchdog from aborting. Thegate-wire.tsconflicts were both non-overlapping additions (CL-5644'srecordDecision/recordOperatorDecisiontranscript calls alongside this PR'sresolvewrapper); resolved by keeping both. Added tests asserting the combined behavior (gate open + active tool call together, and each alone) since neither PR's own tests covered the interaction.Test plan
turn-state.test.ts: pure unit coverage forturnStateGateOpened/turnStateGateClosed, nested (two outstanding) gates, and two regression tests for the cross-turn-boundary leak (gate open across an interrupt-then-resubmit, and a stale gate closing after the turn already settled).turn-monitor.test.ts: stall-watchdog integration tests — a gate open for 20 minutes causes no abort/notice, a gate that is merely queued (not reflected inshell.overlayKind) gets the same exemption, and a gate open alongside a live sibling tool call stays exempt through both the open and the close.stall-watchdog.test.ts: pure-function coverage that the gate exemption and the parallel-tool-call exemption each hold alone and together.bun run test.Review
Went through greybeard, gaasbot, critique, bruckheimer, and neckbeard. Critique and greybeard independently caught a real cross-turn leak in the first pass (the counter reset to zero on every turn-ending transition, letting a still-open gate's later close land against an unrelated turn); fixed and both re-approved after re-review. Neckbeard's one legitimate finding (duplicated once-guarded resolve wrapper) is applied. Bruckheimer confirmed no other blocking-on-human surface in the TUI exists outside
gate-wire.tsthat would need the same wiring.