Skip to content

Fold gate blocked-ness into turn state so the stall watchdog sees it - #381

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-5642-gate-blocked-turns-are-exempted-from-the-stall-watchdog-only
Aug 8, 2026
Merged

Fold gate blocked-ness into turn state so the stall watchdog sees it#381
TheGreatAxios merged 4 commits into
mainfrom
cl-5642-gate-blocked-turns-are-exempted-from-the-stall-watchdog-only

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Turn state now carries a 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 than shell.overlayKind.
  • paintPhase and the stall watchdog now both read bag.turn.status directly — the local, paint-only "blocked" derivation is gone, so the exemption reaches the watchdog instead of stopping at the painter.
  • wireGates accepts optional GateLifecycleHooks (onGateOpened / onGateClosed), wired in product-host.ts to bridge.gateOpened() / bridge.gateClosed().
  • A gate's count now survives a turn boundary (interrupt, or the turn simply settling) instead of being reset to zero, since interrupting/settling never closes the overlay itself — losing count of a still-open gate let its eventual close land against an unrelated later turn, either dropping that turn's own exemption early or reviving a settled turn's status back to "running". Terminal statuses (done/stopped/failed) are left alone when carrying the count forward, since they already satisfy the watchdog's status !== "running" exemption on their own.
  • Deduplicated the once-guarded resolve wrapper shared by onPermission/onOperator in gate-wire.ts into a single onceClosed helper.

Rebase onto main (CL-5641 + CL-5644 merged first)

Rebased onto current main after PR #383 (CL-5641, threads activeToolCalls into ShouldAbortForStallArgs) and PR #382 (CL-5644, transcript echo suppression) landed. Both are independent exemptions feeding the same silentPastThreshold check in stall-watchdog.ts: args.status !== "running" (this PR's gate exemption) is checked before activeToolCalls (CL-5641's exemption), so either one alone — or both together — keeps the watchdog from aborting. The gate-wire.ts conflicts were both non-overlapping additions (CL-5644's recordDecision/recordOperatorDecision transcript calls alongside this PR's resolve wrapper); 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 for turnStateGateOpened / 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 in shell.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.
  • Full suite green through the project's mutex-wrapped 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.ts that would need the same wiring.

@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CL-5642

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
TheGreatAxios force-pushed the cl-5642-gate-blocked-turns-are-exempted-from-the-stall-watchdog-only branch from 91495fa to b3f818c Compare August 8, 2026 16:47
@TheGreatAxios
TheGreatAxios merged commit 7eb560d into main Aug 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant