Plan header
| Field |
Value |
| Status |
IMPLEMENTED |
| Date |
2026-08-22 |
| Type |
single |
| Parent |
N/A |
| Source issue |
#776 — harness: 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 (shouldAutoPromote → tryPromoteQueued), ui.zig:682 idle-empty-▶ on_promote → tryPromoteQueued, ui.zig:747 empty Ctrl+Enter → tryPromoteQueued. Typed send (composer.submitOrEnqueue → bridge.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-rich → test-rich-invariants → harness -Doptimize=Debug → fmt --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 →
shouldAutoPromote → tryPromoteQueued; 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.submitOrEnqueue → bridge.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 |
tryPromoteQueued → canPromote 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 shouldAutoPromote — terminal = 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_promote → ui.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.submitOrEnqueue → bridge.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: submitOrEnqueue → queueSubmitFromUi 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
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).
bridge.zig — add queue_paused latch + isQueuePaused() / setQueuePausedFromUi(); pass queue_paused into tryPromoteQueued's canPromote args; reset with the queue lifecycle.
ui/queue_band.zig — header Pause/Resume toggle (n>0) + visible · paused state; wire the toggle and the empty-queue auto-clear.
- 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.
docs/harness-limits.md Pause row + AGENTS.md one-line note (same PR as the code).
- 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
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 canPromote — queueSubmitFromUi 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
Plan header
harness: pause the submit queue so the next turn comes from the composerdocs/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.
bridge.tryPromoteQueued(editing)(native/harness/src/bridge.zig:303) →submit_queue.canPromote(args struct atnative/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 (shouldAutoPromote→tryPromoteQueued), ui.zig:682 idle-empty-▶on_promote→tryPromoteQueued, ui.zig:747 empty Ctrl+Enter →tryPromoteQueued. Typed send (composer.submitOrEnqueue→bridge.queueSubmitFromUi) bypasses it.shouldAutoPromoteterminal =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_frontexport present (bridge.zig:678). v19queue_promote_allowed/hasQueuePromoteAllowed(bridge.zig:145,274) live only inshouldAutoPromote, NOT incanPromote— so addingpausedtocanPromotecorrectly blocks auto-promote AND explicit idle-▶ Play (goals 2/3), leaving the v19 allowed semantics orthogonal!pausedguard on the already frame-rare promote seam; zero GPA/alloc or host I/O added toui.frame(); no paint-path workqueue_promote_allowedpattern; 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, nobuild.zigwhitelist change, no protocol bump, no wasm-int artifact rebuild. Correctly respects feature-divide (harness canvas owns the queue chrome; no DOM control)submit_queue.test.zigcanPromotepaused guard;bridge.test.zigtryPromoteQueuedblocked + auto-promote held + unpause restores;composer_layout.test.zigidle-▶ / 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. Gatestest-rich→test-rich-invariants→harness -Doptimize=Debug→fmt --check; no TS/vitest — correct for a harness-only diffdocs/harness-limits.mdSubmit-queue Pause row +AGENTS.mdbehavior one-liner; README / SECURITY /.env.exampleN/A justified. Docs are timeless (no phase/issue process artifacts)pausedis 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 requiredn>0; no DOM control; no dual chat; palette-only paused state (TEAL /teal_accent/ optional WARM, never EMBER — syncpalette.zig==lib/palette.ts). Not a host-chrome primary-action layout shift; the queue band already owns Clear/count chromeFindings: 0 blocker · 0 major · 0 minor · 1 nit (pre-existing, non-gating, out of this plan's scope).
docs/feature-divide.mdstill lists the bridge protocol at v19 (thelib/harnessBridge.tsrow and the version-history paragraph mention up throughinv_set_queue_promote_allowedv19); live code andlib/harnessBridge.ts:41are v20 (the additionalinv_queued_insert_frontv20 export from plan plan: harness — turn errors retry the current turn, never drain the queue (#756) · IMPLEMENTED (PR #774) #759/plan: harness — Stop/Esc never drains the queue; idle ▶ promotes when composer empty (#757) #760). This plan correctly does NOT bump the protocol and is not obligated to fix the stale row, so no plan edit is made — but that living doc should catch up to v20 the next time it is touched.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 turnhead on give-up), the operator wants to continue the failed workfrom 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.
Pauseholds 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
tryPromoteQueuedNon-goals / out of scope
Continue the current turn(already a harness: turn errors must retry the current turn — never drain the queue #756 non-goal — park it; the operator types the continue)MAX_ITEMS(submit_queue.zig) /SUBMIT_CAP(bridge.zig) / any capbuild.zigexport-whitelist change)Architectural decisions
The single promote primitive is
bridge.tryPromoteQueued(editing)— everypromote path (successful-Ready auto-promote via
ui.zig→shouldAutoPromote→tryPromoteQueued; idle empty ▶on_promote→tryPromoteQueued; empty Ctrl+Enter →tryPromoteQueued) funnels through it, whichgates on the pure, host-tested
submit_queue.canPromote. Gating that one seamon
!pausedblocks all three promote paths with a single change, while the typedsend path (
composer.submitOrEnqueue→bridge.queueSubmitFromUi) bypasses itentirely and is untouched — exactly the issue's design sketch.
bridge.zigvar queue_paused: bool(reads ontryPromoteQueued) · B)ui/state.zigboolean toggled by the bandbridge.zig(tryPromoteQueued), mirroring the v19queue_promote_allowedlatch. Keeps the gate beside the seam it guards and outside the dvui frame. Wasm-internal (no export)!pausedinto the puresubmit_queue.canPromoteargs struct (single seam all callers converge on) · B) add apausedparam tobridge.shouldAutoPromotetryPromoteQueued→canPromoteis 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.shouldAutoPromotekeeps its success-terminal + host-armedallowedsemantics unchangedinv_set_queue_pausedhost exportbuild.zigwhitelist change, no version bump, no host TS change, no wasm-int artifact rebuildpausedwhenqueuedCount()==0(mirrorshouldDropEditOnEmptyQueue) · B) leave the latch stalen>0· B) composer iconteal_accent; optional WARM emphasis never EMBERpalette.zig==lib/palette.ts)Layer placement
native/harness/src/bridge.zig(queue_pausedvar,setQueuePausedFromUi/isQueuePaused, fold!pausedintotryPromoteQueued);native/harness/src/submit_queue.zig(canPromoteaddspausedguard)native/harness/src/ui/queue_band.zig(header row); optionalui/state.zigframe gluenative/harness/src/submit_queue.test.zig,bridge.test.zig,composer_layout.test.zigcanPromote/tryPromoteQueuedseams are host-testable without a dvui framedocs/harness-limits.mdSubmit queue table;AGENTS.mdqueue rowCurrent baseline (live code)
native/harness/src/bridge.zigshouldAutoPromote—terminal = cur == .ready; abusy → erredge can't popinv_queued_insert_front(protocol v20) /insertQueuedFront/submit_queue.insertFront— only when non-empty and not full, never auto-sentinv_set_queue_promote_allowed(false)/queue_promote_allowed; explicit Play promotesbridge.zig:145,hasQueuePromoteAllowed)ui.zig:~246shouldAutoPromote(… allowed) → tryPromoteQueued(editing)allowed= v19 gateui/composer_chrome.zigon_promote→ui.zig:~680tryPromoteQueued;ui.zig:~739empty Ctrl+Enter →tryPromoteQueuedbridge.tryPromoteQueued(editing)→submit_queue.canPromote(!editing && !busy && !has_pending_submit && !has_pending_load_earlier && count>0) →promoteIf(peek→submit→pop)composer.submitOrEnqueue→bridge.queueSubmitFromUitryPromoteQueued; stays intact while pausedsubmit_queue.{push,pop,insertFront,removeAt,replaceAt,clear}+bridge.{queueSubmitFromUi,enqueueFromUi,clearSubmitQueue,removeQueuedAt,replaceQueuedAt}reset()/inv_clear_messages/ Clear / refresh / New / session switch wipe itbridge.zig PROTOCOL_VERSION = 20;lib/harnessBridge.ts HARNESS_PROTOCOL_VERSION = 20Design
Latch (
bridge.zig):var queue_paused: bool = false, reset to false in thesame lifecycle edges as
queue_promote_allowed(reset(),inv_clear_messages,sessions switch, and a Clear of the FIFO) — the queue and pause share oneWasm-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): addpaused: boolto thecanPromoteargsstruct and
!args.pausedto the guard;bridge.tryPromoteQueuedpasses the livequeue_pausedread. Because auto-promote (ui.zigshouldAutoPromote → tryPromoteQueued),idle empty ▶ (
on_promote → tryPromoteQueued) and empty Ctrl+Enter(
ui.zig:~739 → tryPromoteQueued) all converge ontryPromoteQueued, a singleguard blocks goals 2 and 3; goal 4 follows because unpausing clears the latch and
re-arms the same paths.
Typed send is untouched:
submitOrEnqueue→queueSubmitFromUinever goes throughtryPromoteQueued, so while paused a typed prompt + ▶ / Ctrl+Enter still starts anormal 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, paletteteal_accent/WARM emphasis, never EMBER). Clicking flipssetQueuePausedFromUi—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 ofqueue_band.shouldDropEditOnEmptyQueue), so a stalepause can never block future promotes (goal 4 stays intuitive).
Edge cases
inv_queued_insert_front(Continue) still lands at the head (queue insert, not promote — pause only blocks promote); it stays parked until unpause.canPromote.paused(goal 3); the host's v19allowedis orthogonal and unchanged.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 alreadyruns 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
docs/harness-limits.mdAGENTS.mdREADME.mdSECURITY.md.env.exampleImplementation order
submit_queue.zig— addpausedtocanPromoteargs +!args.pausedguard; add the empty-FIFO auto-clear helper if needed (surfaceshouldDropPauseOnEmptyQueuepredicate alongsideshouldDropEditOnEmptyQueue).bridge.zig— addqueue_pausedlatch +isQueuePaused()/setQueuePausedFromUi(); passqueue_pausedintotryPromoteQueued'scanPromoteargs; reset with the queue lifecycle.ui/queue_band.zig— header Pause/Resume toggle (n>0) + visible· pausedstate; wire the toggle and the empty-queue auto-clear.submit_queue.test.zig(canPromotepaused guard),bridge.test.zig(tryPromoteQueuedblocked while paused, auto-promote held, unpause restores — same host-testable seam family the v19/v20 locks use);composer_layout.test.zigidle ▶ / empty Ctrl+Enter no-op while paused.docs/harness-limits.mdPause row +AGENTS.mdone-line note (same PR as the code).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 (noapp//lib/change). No new bridge export → shipped Wasm artifact unchanged (wasm-int unaffected).Testing
zig build test-rich; manual: one new user line with that text; queue depth unchanged; head not consumedzig build test-rich(composer_layout.test.zig); no promote, no blank rowzig build test-rich(bridge.test.zig); Ready; queue not auto-promotedzig build test-rich; next successful Ready or empty ▶ promotes the head (Continue or original follow-up)zig build test-rich; in-flight turn untouched; enqueue still appends; no promote until unpaused + success/Playsubmit_queue.test.zig; drain/Clear/remove →isQueuePaused()falseMinimum 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 --checkrun in the agent workspace or CI (self-hosted for the release Wasm artifact).Caps table
pausedlatchnative/harness/src/bridge.zig(newqueue_paused)No existing cap is raised or lowered.
MAX_ITEMS(submit queue) = 16 andSUBMIT_CAP= 262144 are unchanged (non-goal). No protocol version bump.
Definition of done
pausedlatch lands inbridge.zig; unfolded!pausedthroughsubmit_queue.canPromotetest-rich/test-rich-invariants/harness -Doptimize=Debug/fmt --check); no TS/vitest neededdocs/harness-limits.mdPause row +AGENTS.mdone-line updated (timeless)Risks & mitigations
· pausedstate on the queue band + auto-clear when the FIFO empties (goal 4 stays intuitive)canPromote—queueSubmitFromUibypasses it; unit test asserts typed send unaffectedshouldAutoPromoteandtryPromoteQueuedtryPromoteQueued/canPromote(the common primitive); noshouldAutoPromotesignature changeOpen 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
harness: pause the submit queue so the next turn comes from the composer