Repository navigation
feat(tui): pause the queue on ctrl+c instead of resuming the run - #1358
TheGreatAxios merged 18 commits into
Conversation
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Request changes
First Ctrl+C pauses the primary and holds the delivery queue, but occupancy/ask-wake still auto-restarts the run, and the stay-alive second press wipes the items that press just held.
Findings
-
src/tui/runtime-bridge.ts:2075—abortInFlightAndHandoffstill callsflushOccupancyThenWake()after the pause gate. Pause only early-returns indrainAtBoundary(:1348) anddrainSteersAtBoundary(:1367). A stashedagent-askor mailbox driver that takes the turn starts a new primary viasendInternalText/beginSystemContinuationwithoutresumeForSend. Same restart if a still-live sub-agent emitsagent-askafter pause. Follow-on: with the run busy again, product Enter is steer and does not clearpaused, so the operator's next message sticks. Verified with a temporary test againsthandleCtrlCplus a stashed ask-wake/mailbox (hard-stop without pause still flushed, as stall/expire should). Permanent regression: first Ctrl+C with stashed ask-wake/mailbox must not deliver;bridge.interrupt()without pause must still flush. -
src/tui/runner/wiring.ts:174— Second Ctrl+C with live sub-agents reusescancelWorkersForStop, which callsbridge.clearQueuedDelivery(). That path doesshell.session = createSessionQueue("idle")(runtime-bridge.ts:2123), dropping held follow-ups and clearingpaused. The first-press flash tells the operator to press Ctrl+C to stop workers; that press then destroys the queue the pause just kept.clearQueuedDeliverybelongs on quit, not on a stay-running stop. Verified: after thecancelAllmicrotask, badge is 0 andpausedis false. Split the queue wipe off the 2nd-press path; keep it on quit. Permanent test: realbuildShellStopAffordance+ bridge, items andisPausedsurvive the stop press. -
src/tui/shell/keys.ts:209—void onStop()swallows nothing. ProductiononStopWorkersiscancelWorkersForStop;cancelAllthrows when a retained close rejects. Second press with a rejecting stop becomes an unhandled rejection; the shell stays in STOPPED with teardown incomplete. Catch/log, or make the affordance sync-safe. Third press during in-flight stop is fine: STOPPED is recorded synchronously (:202), so the next press quits and does not re-enteronStop. -
src/tui/runner/wiring.stop-idempotent.test.ts:82-90— Test named "single-press pause never calls cancelAll" never callshandleCtrlC/pause/interrupt. It only constructs a harness and assertscancelCalls.length === 0. AddingcancelAllto the first-press interrupt path would still pass. Drive the real pause gesture. -
src/tui/shell/keys.ts:265— Comment still saysreadStopAffordance"is NOT yet consulted byhandleCtrlC". It is, at:197and:247. File-for-later, but it will send the next interrupt change hunting for wiring that already landed. -
src/tui/shell/prompt.ts:367-375andsrc/tui/runtime-bridge.ts:1900-1909— Reinject (unwired from product chords) reuses "press ctrl+c again to exit" without arming Ctrl+C. Tests/API only today; misleading if any caller still hits it.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Comment
Pause-hold is a drain latch on the existing interrupt path, not a second interrupt model — but docs/TUI.md still describes the inverted Ctrl+C contract, and two seams will not survive the next interrupt path.
Findings
-
docs/TUI.md:5-7and:739-746,:748-757,:816-830— The file's header says a reviewer holds a PR against it. Those three paragraphs still say: queued work is handed over at the interrupt itself; Ctrl+C is the explicit fleet teardown via agent close; second Ctrl+C quits. This branch inverts all three (hold until explicit send; first presscancelAll-free; live workers make it three-press). Tests do not replace the spec. Update those paragraphs (and the pending-column section at:688-706) to: first press pauses and holds; pending column is the durable surface; held items deliver with the next explicit send; stall/expire still drain; second press stops live workers (app stays up); third quits; no workers keeps two-press.docs/PRODUCT.md:44anddocs/IMPLEMENTATION.md:213still say only "Ctrl+C stops the run". -
src/tui/runner/wiring.ts:168-174viabuildShellStopAffordance(:187-204) — The stay-running 2nd-press stop shares quit teardown, includingclearQueuedDelivery(). Stop-while-running cannot share that wipe. Split it, or the three-press machine cannot keep the pause invariant. (Reviewer verified the drop.) -
src/tui/runner/exit.ts:365-387— Compaction continuation is not held. Pause skips the hop without persisting it; interrupt alsoabortCompaction. The comment's "next boundary re-drives the emit" is a test artifact (a new agent stream re-yields the event). In production the emission is consume-once and gone. Mixing it with operator queue items under onepausedflag will mislead the next continuation emit. Drop the hop on operator pause (matches abort) or persist it as a real pending item the column can show. KeepisPaused?.()short-circuiting beforecontinuationGate.shouldDeliverso a paused skip does not consume the hop. -
src/tui/runtime-bridge.ts:2047-2075—pausedgates drain only. Occupancy/ask-wake can still start a new primary after the operator paused. Either gate those flushes while paused, or document them as an allowed exception so the next interrupt path does not treatpausedas "stay quiet until explicit send". (Reviewer verified the auto-restart.) -
src/tui/shell/keys.ts:182-255— Live workers add a press inside a 2s window. Muscle-memory double-tap now stops workers and does not quit; a slow second tap outside 2s re-arms as a new pause. The count-aware flash expires with the window, so after 2s the only remaining surface is the pending column (if items exist). Call the window and the double-tap change out in TUI.md; do not rely on a 2s flash as the only teacher. Do not add a fourth press.
Ownership itself is sound: ShellStopAffordance matches setShellExitHandler; state.isPaused matches systemNotice. Future interrupt paths must not: set paused off a non-operator abort; drain around the two helpers; put cancelAll on interrupt(); or treat paused as a session-wide quiet latch without gating occupancy.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Approve
First Ctrl+C pauses the run (stops the primary, holds the delivery queue, stays idle until an explicit operator send); live sub-agents make it three-press (2nd stops them, 3rd quits); no workers keeps two-press quit. Stall/expire still drain.
Findings
No findings. Whole-branch review of the assembled 17 commits (b67e6c1..3e81cda) found no blocking interaction: every primary-start path is gated while paused (drainAtBoundary, drainSteersAtBoundary, flushOccupancyThenWake, flushMailboxMail, flushPendingAskWake, beginSystemContinuation), stay-alive 2nd-press stop keeps the held queue (cancelLiveWorkers), quit/shutdown still full-teardown (cancelWorkersForStop), three-press machine is idempotent, compaction hop is deferred (consume-once-intact) and delivers after resume. bun run check: 8372 pass / 0 fail.
Track (not blocking): compaction continuation re-dispatch after resume depends on the governor re-triggering over threshold; the cross-phase test drives re-delivery via an explicit rebuild, and production may depend on the next send. wiring.stop-idempotent.test.ts header comment and the reinject-path "press ctrl+c again to exit" copy are stale.
3e81cda to
f8d8837
Compare
Pause already holds the delivery queue. Occupancy and ask-wake still started a new primary after Ctrl+C. Stall and expire do not pause, so they still flush.
Stay-alive second Ctrl+C cancelled workers through the quit teardown, which wiped held follow-ups. Quit still wipes; stay-alive does not.
f8d8837 to
f8e0b57
Compare
Summary
Verification
bun run checkpasses on the whole branch: 8364 pass, 0 fail across 542 files (lint, typecheck, dead-exports, build, guarded test suite).Fixes CL-10149