Skip to content

feat(tui): pause the queue on ctrl+c instead of resuming the run - #1358

Merged
TheGreatAxios merged 18 commits into
mainfrom
cl-10149-ctrlc-stops-the-agent-loop-then-auto-restarts-queued-follow
Oct 11, 2026
Merged

TheGreatAxios merged 18 commits into
mainfrom
cl-10149-ctrlc-stops-the-agent-loop-then-auto-restarts-queued-follow

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • A single Ctrl+C now pauses the run: it stops the primary agent, holds the delivery queue (Alt+Enter follow-ups and compaction continuations stay pending), and shows a pause note instead of "stop — restarting from your message".
  • When sub-agents are running, a second Ctrl+C stops them and keeps the app running; the pause note is count-aware ("N sub-agent(s) running — press ctrl+c to stop, again to exit"). A third Ctrl+C quits.
  • When no sub-agents are running, the second Ctrl+C still quits (two-press preserved).
  • Queued work delivers only on an explicit new send, never automatically to a rebuilt agent after an interrupt. Non-operator interrupt paths (stall/expire aborts) keep their prior drain behavior; stopping sub-agents is idempotent (no double-cancel on stop-then-quit).

Verification

  • bun run check passes on the whole branch: 8364 pass, 0 fail across 542 files (lint, typecheck, dead-exports, build, guarded test suite).
  • The follow-up integration test drives the real registered stop closure through the Ctrl+C state machine against a harnessed store: workers drain to 0 live, the app stays running, and the count is reported.
  • Each commit was reviewed individually before landing; the branch has 11 commits (pause semantics + sub-agent stop follow-up).

Fixes CL-10149

@linear-code

linear-code Bot commented Oct 8, 2026

Copy link
Copy Markdown

CL-10149

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — abortInFlightAndHandoff still calls flushOccupancyThenWake() after the pause gate. Pause only early-returns in drainAtBoundary (:1348) and drainSteersAtBoundary (:1367). A stashed agent-ask or mailbox driver that takes the turn starts a new primary via sendInternalText / beginSystemContinuation without resumeForSend. Same restart if a still-live sub-agent emits agent-ask after pause. Follow-on: with the run busy again, product Enter is steer and does not clear paused, so the operator's next message sticks. Verified with a temporary test against handleCtrlC plus 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 reuses cancelWorkersForStop, which calls bridge.clearQueuedDelivery(). That path does shell.session = createSessionQueue("idle") (runtime-bridge.ts:2123), dropping held follow-ups and clearing paused. The first-press flash tells the operator to press Ctrl+C to stop workers; that press then destroys the queue the pause just kept. clearQueuedDelivery belongs on quit, not on a stay-running stop. Verified: after the cancelAll microtask, badge is 0 and paused is false. Split the queue wipe off the 2nd-press path; keep it on quit. Permanent test: real buildShellStopAffordance + bridge, items and isPaused survive the stop press.

  • src/tui/shell/keys.ts:209 — void onStop() swallows nothing. Production onStopWorkers is cancelWorkersForStop; cancelAll throws 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-enter onStop.

  • src/tui/runner/wiring.stop-idempotent.test.ts:82-90 — Test named "single-press pause never calls cancelAll" never calls handleCtrlC / pause / interrupt. It only constructs a harness and asserts cancelCalls.length === 0. Adding cancelAll to the first-press interrupt path would still pass. Drive the real pause gesture.

  • src/tui/shell/keys.ts:265 — Comment still says readStopAffordance "is NOT yet consulted by handleCtrlC". It is, at :197 and :247. File-for-later, but it will send the next interrupt change hunting for wiring that already landed.

  • src/tui/shell/prompt.ts:367-375 and src/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 TheGreatAxios left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-7 and :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 press cancelAll-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:44 and docs/IMPLEMENTATION.md:213 still say only "Ctrl+C stops the run".

  • src/tui/runner/wiring.ts:168-174 via buildShellStopAffordance (:187-204) — The stay-running 2nd-press stop shares quit teardown, including clearQueuedDelivery(). 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 also abortCompaction. 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 one paused flag 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. Keep isPaused?.() short-circuiting before continuationGate.shouldDeliver so a paused skip does not consume the hop.

  • src/tui/runtime-bridge.ts:2047-2075 — paused gates 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 treat paused as "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 TheGreatAxios left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@TheGreatAxios
TheGreatAxios force-pushed the cl-10149-ctrlc-stops-the-agent-loop-then-auto-restarts-queued-follow branch from 3e81cda to f8d8837 Compare October 11, 2026 01:26
@TheGreatAxios
TheGreatAxios force-pushed the cl-10149-ctrlc-stops-the-agent-loop-then-auto-restarts-queued-follow branch from f8d8837 to f8e0b57 Compare October 11, 2026 01:26
@TheGreatAxios
TheGreatAxios merged commit 7af83cd into main Oct 11, 2026
13 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-10149-ctrlc-stops-the-agent-loop-then-auto-restarts-queued-follow branch October 11, 2026 01:39
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