Summary
Stop (■) and Esc cancel the in-flight turn and today look like a finished turn: Wasm sees busy → ready and tryPromoteQueued starts the next queued prompt. The operator asked to stop; the harness keeps going.
Stop must only stop. The queue stays put. Consuming the next item is an explicit Play: idle ▶ with items in the queue, even when the composer is empty.
Amends #756: that issue’s “Stop still drains after Ready” is wrong. Stop never promotes. Play does.
Current baseline (live code)
| Claim |
Path / symbol |
Notes |
| Promote on any terminal |
native/harness/src/ui.zig ~227–235 |
terminal = ready or err; busy → terminal → tryPromoteQueued (success, error, and Stop all go Ready) |
| Docs |
docs/harness-limits.md Submit queue |
When it runs: Ready or error. Stop: “queue stays and drains after Ready” — this is the defect |
| Idle ▶ |
ui/composer_chrome.zig |
if (res.typed.len > 0) on_send — empty composer is a no-op |
| Ctrl/Cmd+Enter idle |
composer.submitOrEnqueue / keymap submit |
Blank normalize → clearPrompt and return; does not promote |
| Stop / Esc |
■ on_stop → queueCancelFromUi; Esc cancel_turn while Busy |
Host abort → Ready → same promote path as success |
Goals
| # |
Goal |
Success signal |
| 1 |
■ Stop and Esc (Busy, not queue-edit / help) do not promote |
Queue depth unchanged; no new user row from the old head; composer idle; ■ gone, ▶ back |
| 2 |
Idle ▶ with queue non-empty and empty composer promotes the head |
One click starts that prompt as a normal user turn (pending_submit / queueSubmitFromUi); head popped; Busy |
| 3 |
Idle ▶ with non-empty composer still sends the composer text (today) |
Queue untouched until that turn’s successful Ready (see #756) |
| 4 |
Idle ▶ with empty composer and empty queue stays a no-op |
No blank user row (existing empty-send guard) |
Non-goals
- Do not auto-drain the queue after Stop, error, or cancel
- Do not change Busy ▶ (still enqueue when the field has text)
- Do not consume the queue on error — that’s #756 (retry / Continue-at-head)
- Do not add a second “Play queue” icon; the existing idle ▶ is the control
- Forbidden wiring: dual DOM chat · secrets in Wasm
Design
Stop / Esc — promote only after successful Ready, never after cancel
tryPromoteQueued on busy → ready must not run when the turn ended because of Stop. Same gate #756 needs for error.
Practical split:
Host already has classifyTurnFailure (stop vs error vs timeout). Wasm today cannot tell success Ready from Stop Ready — both are .ready. Options (pick one in implementation, lock in the PR):
- Host sets a one-frame “do not promote” flag / distinct lifecycle (
.err already exists; Stop could stay Ready with a inv_* inhibit, or use Error only for failures and a boolean promote_ok)
- Promote moves off the lifecycle edge and onto an explicit Play only (goal 2). Then success would also not auto-drain — operator hits ▶ to continue the queue after every turn.
Operator intent for this issue: Stop must not play the next item. After a successful turn, auto-promote may remain (today / #756 goal 1). After Stop, only ▶ starts the next item.
Idle ▶ — empty field + queue → promote
In paintComposerChrome idle branch (and the request_submit / submitOrEnqueue blank path):
if composer empty and idle and queuedCount > 0:
tryPromoteQueued(editing)
else if composer non-empty:
submitOrEnqueue(text) // today
else:
no-op
▶ stays enabled when the field is empty iff the queue is non-empty (visual: don’t grey the icon in that state). Ctrl/Cmd+Enter should do the same (it’s the same submit action) so keyboard and Play match.
submit_queue.canPromote already requires !busy && count > 0 && !editing && !pending_submit. Reuse it.
Living docs
| Surface |
Change |
docs/harness-limits.md Submit queue Stop |
Stop cancels only the in-flight turn. Queue does not drain. Idle ▶ (empty composer) starts the head |
| Same table When it runs |
Auto-promote on successful Ready only (not Stop, not error). Manual promote: idle ▶ / Ctrl+Enter with empty field + non-empty queue |
| Keyboard & focus |
Esc Busy = stop, no queue consume |
AGENTS.md / README / SECURITY |
N/A unless a new cap/export is added |
Testing
| # |
Case |
Expected |
| 1 |
Busy + 2 queued + ■ Stop |
Ready; queue still 2; no new user line from head |
| 2 |
Same + Esc |
Same as 1 |
| 3 |
After 1, idle empty composer, click ▶ |
Head submits; queue 1; Busy |
| 4 |
After 1, type text, click ▶ |
Sends the typed prompt (enqueue-not, idle send); queue still 2 |
| 5 |
Idle, empty composer, empty queue, ▶ |
No-op |
| 6 |
Queue-row editor open + Esc |
Still dismisses the editor, not the turn / not promote (existing precedence) |
submit_queue unit tests + composer/keymap host tests for empty-submit-promotes. zig build test-rich · vitest if the host must pass a Stop-vs-success promote inhibit.
Cloud ops
N/A — Wasm queue + host abort; no Production mutate.
Refs #756 (errors / retry) · submit-queue #664 · Esc cancel_turn #741
Summary
Stop (■) and Esc cancel the in-flight turn and today look like a finished turn: Wasm sees
busy → readyandtryPromoteQueuedstarts the next queued prompt. The operator asked to stop; the harness keeps going.Stop must only stop. The queue stays put. Consuming the next item is an explicit Play: idle ▶ with items in the queue, even when the composer is empty.
Amends #756: that issue’s “Stop still drains after Ready” is wrong. Stop never promotes. Play does.
Current baseline (live code)
native/harness/src/ui.zig~227–235terminal = ready or err;busy → terminal→tryPromoteQueued(success, error, and Stop all go Ready)docs/harness-limits.mdSubmit queueui/composer_chrome.zigif (res.typed.len > 0) on_send— empty composer is a no-opcomposer.submitOrEnqueue/ keymapsubmitclearPromptand return; does not promoteon_stop→queueCancelFromUi; Esccancel_turnwhile BusyGoals
pending_submit/queueSubmitFromUi); head popped; BusyNon-goals
Design
Stop / Esc — promote only after successful Ready, never after cancel
tryPromoteQueuedonbusy → readymust not run when the turn ended because of Stop. Same gate #756 needs for error.Practical split:
done→ Ready → promote (if queue non-empty and not editing)Host already has
classifyTurnFailure(stopvserrorvs timeout). Wasm today cannot tell success Ready from Stop Ready — both are.ready. Options (pick one in implementation, lock in the PR):.erralready exists; Stop could stay Ready with ainv_*inhibit, or use Error only for failures and a booleanpromote_ok)Operator intent for this issue: Stop must not play the next item. After a successful turn, auto-promote may remain (today / #756 goal 1). After Stop, only ▶ starts the next item.
Idle ▶ — empty field + queue → promote
In
paintComposerChromeidle branch (and therequest_submit/submitOrEnqueueblank path):▶ stays enabled when the field is empty iff the queue is non-empty (visual: don’t grey the icon in that state). Ctrl/Cmd+Enter should do the same (it’s the same
submitaction) so keyboard and Play match.submit_queue.canPromotealready requires!busy && count > 0 && !editing && !pending_submit. Reuse it.Living docs
docs/harness-limits.mdSubmit queue StopAGENTS.md/ README / SECURITYTesting
submit_queueunit tests + composer/keymap host tests for empty-submit-promotes.zig build test-rich· vitest if the host must pass a Stop-vs-success promote inhibit.Cloud ops
N/A — Wasm queue + host abort; no Production mutate.
Refs #756 (errors / retry) · submit-queue #664 · Esc
cancel_turn#741