Skip to content

plan: pause the submit queue so the next turn reads from the composer #777

Description

@btipling

Plan header

Field Value
Status IMPLEMENTED
Date 2026-08-22
Type single
Parent N/A
Source issue #776harness: pause the submit queue so the next turn comes from the composer
Branch plan/queue-pause
Layers harness (Wasm) only
Reusability impact none — in-canvas submit-queue operator state; no env / tenant / host seam
Production mutate? no
Cloud ops path N/A — no Production mutate
Living docs docs/harness-limits.md (Submit queue) · AGENTS.md (queue row, behavior note)

Review notes (2026-08-22)

Verdict: HANDOFF-READY — reviewed by plan-review (adversarial, baseline verified against live code). No Blockers, no Majors, no Minors. One non-gating pre-existing Nit. No plan content changes required; Status written to HANDOFF-READY on the issue.

Axis Score (1–5) Notes
Correctness 5 Baseline fully grounded: bridge.tryPromoteQueued(editing) (native/harness/src/bridge.zig:303) → submit_queue.canPromote (args struct at native/harness/src/submit_queue.zig:126; guard !editing && !busy && !has_pending_submit && !has_pending_load_earlier && count>0). All three promote paths converge on it: ui.zig:254 auto-promote (shouldAutoPromotetryPromoteQueued), ui.zig:682 idle-empty-▶ on_promotetryPromoteQueued, ui.zig:747 empty Ctrl+Enter → tryPromoteQueued. Typed send (composer.submitOrEnqueuebridge.queueSubmitFromUi) bypasses it. shouldAutoPromote terminal = cur == .ready (ui.zig:294). PROTOCOL_VERSION = 20 (bridge.zig:44) == HARNESS_PROTOCOL_VERSION = 20 (lib/harnessBridge.ts:41) — no bump needed. inv_queued_insert_front export present (bridge.zig:678). v19 queue_promote_allowed/hasQueuePromoteAllowed (bridge.zig:145,274) live only in shouldAutoPromote, NOT in canPromote — so adding paused to canPromote correctly blocks auto-promote AND explicit idle-▶ Play (goals 2/3), leaving the v19 allowed semantics orthogonal
Performance 5 Bool latch + one !paused guard on the already frame-rare promote seam; zero GPA/alloc or host I/O added to ui.frame(); no paint-path work
Architecture 5 Single-seam gating mirrors the v19 queue_promote_allowed pattern; cleanly blocks all three promote paths with one change while leaving typed send + FIFO ops (enqueue/edit/remove/Clear) intact (goal 6). Wasm-internal — no host export, no build.zig whitelist change, no protocol bump, no wasm-int artifact rebuild. Correctly respects feature-divide (harness canvas owns the queue chrome; no DOM control)
Testing 4 Zig host-unit suite (submit_queue.test.zig canPromote paused guard; bridge.test.zig tryPromoteQueued blocked + auto-promote held + unpause restores; composer_layout.test.zig idle-▶ / empty Ctrl+Enter no-op) + operator/browser rows (5, 7). Minimum locked rows 1,2,3,4,6 via the same host-testable seam family the v19/v20 locks use. Gates test-richtest-rich-invariantsharness -Doptimize=Debugfmt --check; no TS/vitest — correct for a harness-only diff
Cloud ops N/A No Production mutate — pause is in-canvas Wasm queue state; N/A justified and stated
Living docs 4 docs/harness-limits.md Submit-queue Pause row + AGENTS.md behavior one-liner; README / SECURITY / .env.example N/A justified. Docs are timeless (no phase/issue process artifacts)
Cap governance 5 paused is a boolean hold, not a numeric cap; CAPS table present. NO existing cap raised or lowered (MAX_ITEMS = 16, SUBMIT_CAP = 262144 unchanged) → no BLOCK, no human cap decision required
Parent adherence N/A Single plan, no parent
Layer / UI 5 Harness-only; in-canvas queue-band header toggle visible when n>0; no DOM control; no dual chat; palette-only paused state (TEAL / teal_accent / optional WARM, never EMBER — sync palette.zig == lib/palette.ts). Not a host-chrome primary-action layout shift; the queue band already owns Clear/count chrome
Reusability 5 Pure in-canvas submit-queue operator state; no env / tenant / host seam; no single-owner hardcoding

Findings: 0 blocker · 0 major · 0 minor · 1 nit (pre-existing, non-gating, out of this plan's scope).

Verdict after edits: HANDOFF-READY — no plan content changes required; Status confirmed HANDOFF-READY on the issue (+ Reviewed stamp added).

Summary

Add an operator-initiated pause latch to the Wasm submit-queue. After a turn
error (#756 / PR #774 already park the queue and insert a Continue the current turn head on give-up), the operator wants to continue the failed work
from the composer
— a typed correction / smaller retry — without the next
send (auto-promote on a later successful Ready, or an idle empty ▶ / empty
Ctrl+Enter) consuming a parked queue item. Pause holds every promote path
(auto-promote + explicit empty-▶ Play) while keeping the FIFO contents and the
typed-send path untouched: the next turn reads directly from the composer. The
operator bumps a visible in-canvas pause/resume toggle in the queue band to
unpause; only then do parked items drain by the existing rules.

Goals

# Goal Success signal
1 Operator can pause the submit queue (idle, including after Error / Stop) Visible paused state in the queue band; FIFO contents unchanged (no pop, no insert, no clear)
2 While paused, the next turn comes from the composer Typed prompt + ▶ / Ctrl+Enter sends that text. Empty ▶ / empty Ctrl+Enter does not tryPromoteQueued
3 While paused, a successful Ready does not auto-promote Continue-turn succeeds → Ready → queue depth unchanged; head still the same row
4 Operator can unpause; parked items drain by the existing rules Unpause + successful Ready (or explicit empty ▶) promotes the head as today
5 Pause does not cancel an in-flight turn and does not retry In-flight Busy / Stop / #756 retry path unchanged. Pause is a queue hold, not a turn control
6 Enqueue / edit / remove / Clear still work while paused Follow-ups can still be parked or dropped; pause only blocks promote

Non-goals / out of scope

Architectural decisions

The single promote primitive is bridge.tryPromoteQueued(editing) — every
promote path (successful-Ready auto-promote via ui.zig
shouldAutoPromotetryPromoteQueued; idle empty ▶ on_promote
tryPromoteQueued; empty Ctrl+Enter → tryPromoteQueued) funnels through it, which
gates on the pure, host-tested submit_queue.canPromote. Gating that one seam
on !paused blocks all three promote paths with a single change, while the typed
send path (composer.submitOrEnqueuebridge.queueSubmitFromUi) bypasses it
entirely and is untouched — exactly the issue's design sketch.

Decision Options considered Choice Why
Where the paused latch lives A) bridge.zig var queue_paused: bool (reads on tryPromoteQueued) · B) ui/state.zig boolean toggled by the band A The terminal promote logic lives in bridge.zig (tryPromoteQueued), mirroring the v19 queue_promote_allowed latch. Keeps the gate beside the seam it guards and outside the dvui frame. Wasm-internal (no export)
How pause blocks promote A) Fold !paused into the pure submit_queue.canPromote args struct (single seam all callers converge on) · B) add a paused param to bridge.shouldAutoPromote A tryPromoteQueuedcanPromote is the ONE place auto-promote, idle ▶ and empty Ctrl+Enter already meet; one guard + one host-unit-test row covers all three goals 2/3/4. shouldAutoPromote keeps its success-terminal + host-armed allowed semantics unchanged
Is a host bridge export / protocol bump needed A) none (Wasm-internal latch + in-canvas toggle) · B) inv_set_queue_paused host export A Issue says in-canvas queue-band control, Wasm-ephemeral, not DOM. The host never needs to know; no build.zig whitelist change, no version bump, no host TS change, no wasm-int artifact rebuild
Pause + empty queue A) auto-clear paused when queuedCount()==0 (mirror shouldDropEditOnEmptyQueue) · B) leave the latch stale A With nothing to hold, a stale pause would silently block later promotes after all items drain. Auto-clear keeps promote re-armed on an empty FIFO (goal 4 stays intuitive)
Unpause behavior A) unpause only clears the latch (next successful Ready / explicit empty ▶ drains) · B) unpause immediately promotes the head A Matches the issue's goal 4 exactly ("Unpause + successful Ready (or explicit empty ▶)"). Immediate-promote would surprise by starting a turn the operator didn't send
Toggle control A) pause/resume button on the queue-band header row (next to Clear), visible when n>0 · B) composer icon A The queue band already owns queue chrome (Count · full · Clear). A toggle beside Clear is the discoverable, in-canvas surface the issue calls for
Palette of the paused state TEAL chrome; active-pause label in teal_accent; optional WARM emphasis never EMBER EMBER is danger-only; pause is a hold, not an error. Palette-only per AGENTS.md (sync palette.zig == lib/palette.ts)

Layer placement

Concern Layer Path(s) Rationale
Pause latch + promote gate harness native/harness/src/bridge.zig (queue_paused var, setQueuePausedFromUi/isQueuePaused, fold !paused into tryPromoteQueued); native/harness/src/submit_queue.zig (canPromote adds paused guard) Queue + promote logic already live here; no host wire
Pause/resume toggle + paused paint harness native/harness/src/ui/queue_band.zig (header row); optional ui/state.zig frame glue In-canvas queue chrome per issue (not a DOM control)
Zig unit tests harness native/harness/src/submit_queue.test.zig, bridge.test.zig, composer_layout.test.zig canPromote/tryPromoteQueued seams are host-testable without a dvui frame
Docs docs docs/harness-limits.md Submit queue table; AGENTS.md queue row Durable product truth

Current baseline (live code)

Claim Path / symbol Notes
Error never auto-promotes native/harness/src/bridge.zig shouldAutoPromoteterminal = cur == .ready; a busy → err edge can't pop verified
Give-up inserts Continue at head inv_queued_insert_front (protocol v20) / insertQueuedFront / submit_queue.insertFront — only when non-empty and not full, never auto-sent verified
Stop never drains plan #760 inv_set_queue_promote_allowed(false) / queue_promote_allowed; explicit Play promotes verified (bridge.zig:145, hasQueuePromoteAllowed)
Successful Ready auto-promotes ui.zig:~246 shouldAutoPromote(… allowed) → tryPromoteQueued(editing) host-armed allowed = v19 gate
Idle empty ▶ / empty Ctrl+Enter ui/composer_chrome.zig on_promoteui.zig:~680 tryPromoteQueued; ui.zig:~739 empty Ctrl+Enter → tryPromoteQueued same primitive as auto-promote
Single promote primitive bridge.tryPromoteQueued(editing)submit_queue.canPromote (!editing && !busy && !has_pending_submit && !has_pending_load_earlier && count>0) → promoteIf (peek→submit→pop) verified — all three promote paths converge here
Typed send path composer.submitOrEnqueuebridge.queueSubmitFromUi bypasses tryPromoteQueued; stays intact while paused
FIFO ops submit_queue.{push,pop,insertFront,removeAt,replaceAt,clear} + bridge.{queueSubmitFromUi,enqueueFromUi,clearSubmitQueue,removeQueuedAt,replaceQueuedAt} untouched; enqueue/edit/remove/Clear keep working while paused (goal 6)
Queue lifetime Wasm-ephemeral — reset() / inv_clear_messages / Clear / refresh / New / session switch wipe it pause latch resets with the queue
Protocol version bridge.zig PROTOCOL_VERSION = 20; lib/harnessBridge.ts HARNESS_PROTOCOL_VERSION = 20 no bump — pause is Wasm-internal, no new export

Design

Latch (bridge.zig): var queue_paused: bool = false, reset to false in the
same lifecycle edges as queue_promote_allowed (reset(), inv_clear_messages,
sessions switch, and a Clear of the FIFO) — the queue and pause share one
Wasm-ephemeral lifetime. Getters/setters used only by the in-canvas toggle:
isQueuePaused(), setQueuePausedFromUi(bool) (no cast export → no build.zig /
protocol change).

Promote gate (submit_queue.zig): add paused: bool to the canPromote args
struct and !args.paused to the guard; bridge.tryPromoteQueued passes the live
queue_paused read. Because auto-promote (ui.zig shouldAutoPromote → tryPromoteQueued),
idle empty ▶ (on_promote → tryPromoteQueued) and empty Ctrl+Enter
(ui.zig:~739 → tryPromoteQueued) all converge on tryPromoteQueued, a single
guard blocks goals 2 and 3; goal 4 follows because unpausing clears the latch and
re-arms the same paths.

Typed send is untouched: submitOrEnqueuequeueSubmitFromUi never goes through
tryPromoteQueued, so while paused a typed prompt + ▶ / Ctrl+Enter still starts a
normal turn from the composer (goal 2).

Toggle + paint (ui/queue_band.zig): on the header row (count · [full] · Pause/Resume
· Clear), only when n>0. Active pause shows a clear text/label (· paused, palette
teal_accent/WARM emphasis, never EMBER). Clicking flips setQueuePausedFromUi
pause then unpause. Enqueue/edit/remove/Clear buttons keep working while paused
(goal 6) because they are separate bridge calls that never consult the latch.

Empty-queue auto-clear: when a drain / Clear / remove leaves queuedCount()==0,
clear queue_paused (mirror of queue_band.shouldDropEditOnEmptyQueue), so a stale
pause can never block future promotes (goal 4 stays intuitive).

Edge cases

  • Pause while Busy: in-flight turn untouched (goal 5); busy ▶ still enqueues to the FIFO; nothing promotes until unpaused and a successful Ready / explicit Play.
  • Paused give-up: a host inv_queued_insert_front (Continue) still lands at the head (queue insert, not promote — pause only blocks promote); it stays parked until unpause.
  • Paused successful Ready: auto-promote suppressed by canPromote.paused (goal 3); the host's v19 allowed is orthogonal and unchanged.
  • Pause with a full queue: unchanged — enqueues still rejected; pause only blocks promote.
  • Empty composer + queue while paused: idle ▶ and empty Ctrl+Enter no-op (no blank row, no promote) — same empty-FIFO fail-closed as today, plus the pause guard.
  • Refresh / New / Clear / session switch: pause latch gone with the queue (non-goal; Wasm-ephemeral).

Frame budget: the latch is a bool read/write; the toggle is a normal dvui button.
Zero GPA/alloc or host I/O added to ui.frame() — the promoted-head path already
runs promote on the bridge, so no paint-path work is added.

Cloud ops path

N/A — no Production mutate. Pause is in-canvas Wasm queue state.

Living docs plan

Surface Change Notes
docs/harness-limits.md Submit queue table — add a Pause row: while paused the next turn reads from the composer; auto-promote on successful Ready and idle empty ▶ / empty Ctrl+Enter are held; FIFO contents + enqueue/edit/remove/Clear unaffected; pause is a queue hold (no cancel/retry); unpause restores drain-by-existing-rules; Wasm-ephemeral like the queue timeless; no phase/issue process artifacts
AGENTS.md Submit-queue row (Structure table) — one line: operator pause latch holds all promote paths (auto + empty-▶ Play); no new export/cap, protocol stays v20 behavior-only; no new export/cap → caps table stays empty
README.md N/A — no visitor-facing change
SECURITY.md N/A — no secrets / trust boundary / cutover
.env.example N/A — no new env

Implementation order

  1. submit_queue.zig — add paused to canPromote args + !args.paused guard; add the empty-FIFO auto-clear helper if needed (surface shouldDropPauseOnEmptyQueue predicate alongside shouldDropEditOnEmptyQueue).
  2. bridge.zig — add queue_paused latch + isQueuePaused() / setQueuePausedFromUi(); pass queue_paused into tryPromoteQueued's canPromote args; reset with the queue lifecycle.
  3. ui/queue_band.zig — header Pause/Resume toggle (n>0) + visible · paused state; wire the toggle and the empty-queue auto-clear.
  4. Zig unit tests: submit_queue.test.zig (canPromote paused guard), bridge.test.zig (tryPromoteQueued blocked while paused, auto-promote held, unpause restores — same host-testable seam family the v19/v20 locks use); composer_layout.test.zig idle ▶ / empty Ctrl+Enter no-op while paused.
  5. docs/harness-limits.md Pause row + AGENTS.md one-line note (same PR as the code).
  6. Gates: zig build test-rich · zig build test-rich-invariants · zig build harness -Doptimize=Debug (full Wasm compile gate) · zig fmt --check <touched zig>. No TS/vitest (no app//lib/ change). No new bridge export → shipped Wasm artifact unchanged (wasm-int unaffected).

Testing

# Case Layer Type Command / method
1 Error + non-empty queue + pause + type “continue” + send harness unit + operator zig build test-rich; manual: one new user line with that text; queue depth unchanged; head not consumed
2 Paused + empty idle ▶ / empty Ctrl+Enter harness unit + operator zig build test-rich (composer_layout.test.zig); no promote, no blank row
3 Paused continue-turn succeeds harness unit + operator zig build test-rich (bridge.test.zig); Ready; queue not auto-promoted
4 Unpause after (3) harness unit + operator zig build test-rich; next successful Ready or empty ▶ promotes the head (Continue or original follow-up)
5 Pause while Busy harness unit + operator zig build test-rich; in-flight turn untouched; enqueue still appends; no promote until unpaused + success/Play
6 Pause with empty queue auto-clears harness unit submit_queue.test.zig; drain/Clear/remove → isQueuePaused() false
7 Refresh / New / Clear / session switch harness operator Pause latch gone with the queue (Wasm-ephemeral)

Minimum locked rows (DoD): 1, 2, 3, 4 (promote hold + unpause drain) via the Zig
host-unit suite; 6 (empty-queue auto-clear). 5 and 7 are operator/manual (browser)
rows, consistent with the #762/#775 shift-click / read-only-select operator gates.
Gates command: zig build test-rich && zig build test-rich-invariants && zig build harness -Doptimize=Debug && zig fmt --check run in the agent workspace or CI (self-hosted for the release Wasm artifact).

Caps table

Cap / ceiling Value Rationale Code location
paused latch bool (0/1) No numeric budget; a boolean hold native/harness/src/bridge.zig (new queue_paused)

No existing cap is raised or lowered. MAX_ITEMS (submit queue) = 16 and SUBMIT_CAP
= 262144 are unchanged (non-goal). No protocol version bump.

Definition of done

  • paused latch lands in bridge.zig; unfolded !paused through submit_queue.canPromote
  • Typed composer send works while paused (goal 2); auto-promote + empty-▶ Play held (goals 2/3)
  • Pause/resume toggle paints on the queue band with a visible paused state (goal 1), palette-only, no EMBER
  • Unpause re-arms drain by existing rules (goal 4); in-flight Busy/Stop/harness: turn errors must retry the current turn — never drain the queue #756 retry untouched (goal 5)
  • Enqueue/edit/remove/Clear work while paused (goal 6)
  • Tests green (Zig suite listed above) in agent workspace or CI (test-rich / test-rich-invariants / harness -Doptimize=Debug / fmt --check); no TS/vitest needed
  • Cloud ops: N/A — no Production mutate
  • Living docs: docs/harness-limits.md Pause row + AGENTS.md one-line updated (timeless)
  • No dual-chat regression / no DOM pause control / no protocol bump / no new cap (non-goals respected)
  • AGENTS.md / README considered (README N/A justified; AGENTS behavior note)

Risks & mitigations

Risk Mitigation
A stale pause silently blocks all promotion (operator forgets they paused) Visible · paused state on the queue band + auto-clear when the FIFO empties (goal 4 stays intuitive)
Pause accidentally leaks into the typed-send path Gate lives only in canPromotequeueSubmitFromUi bypasses it; unit test asserts typed send unaffected
Diverging gate between shouldAutoPromote and tryPromoteQueued Latch only tryPromoteQueued/canPromote (the common primitive); no shouldAutoPromote signature change
No protocol/export/artifact drift Pause is Wasm-internal (no export, no bump, no build.zig whitelist change, no wasm-int rebuild)
Reusability / single-owner hardcode None — pure in-canvas queue state; no env/tenant/host seam
Laptop-only cutover / missing GHA N/A — no Production mutate; harness build gate runs in agent workspace or CI (self-hosted for the release Wasm)

Open questions

None — in-scope engineering choices are locked above. (No user decision required; this
does not change any existing cap or add operator surface friction — the standing
no-user-gates stance is respected: pausing is an optional operator hold, not a required
confirmation.)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions