Summary
A turn error today looks like a finished turn: the host sets Ready, Wasm treats busy → ready|err as terminal, and tryPromoteQueued pops the queue head into a new user send. The failed work is abandoned; the operator’s follow-up runs instead.
That is wrong. Errors retry the current task (exponential backoff, 5 attempts) or stop. They never consume queue items.
When retries are exhausted and the queue is non-empty, insert a “Continue the current turn” prompt as the new queue head. Do not pop anything already queued.
Current baseline (live code)
| Claim |
Path / symbol |
Notes |
| Promote on Ready or error |
native/harness/src/ui.zig ~227–235 |
terminal = ready or err; trigger_a = prev busy and terminal → bridge.tryPromoteQueued |
| Docs agree (and are the bug) |
docs/harness-limits.md Submit queue → When it runs |
“After the current turn reaches Ready or error, the head submits” |
| Promote pops head |
submit_queue.promoteIf / bridge.tryPromoteQueued |
Peek → queueSubmitFromUi → pop |
| Fail path sets Ready |
lib/harnessChat.ts runHarnessChat / agent fail |
Error line + setLifecycle(Ready) — same as success, so promote fires |
| Stop vs error |
classifyTurnFailure |
499 / abort → stop; timeout / empty / other → error |
| Existing backoff seam |
lib/sandbox/resilience.ts withTransientRetry |
5 total attempts (retries default 4 after the first), exponential baseMs / capMs, abort cancels sleep. Used for sandbox attach, not the turn |
| Queue |
submit_queue.zig |
FIFO push at tail only — no insert-at-0 yet. Cap 16, Wasm-ephemeral |
Operator report: turn errors with queued follow-ups abort the current message and start the queued one.
Goals
| # |
Goal |
Success signal |
| 1 |
A turn error never promotes the operator queue |
Busy + 1+ queued items + /api/agent failure (after retries) → queue depth unchanged; no new user row from the old head |
| 2 |
Retry the same prompt 5 times with exponential backoff |
5 attempts total (1 + 4 retries), same rawPrompt / session, no extra user ring lines. Stay Busy for the whole retry window so promote cannot run |
| 3 |
Give up after 5 → stop |
One Error (or timeout) turn-end line; lifecycle idle; Stop chrome off |
| 4 |
If the queue is non-empty on give-up, unshift Continue the current turn as the new head |
Existing items shift down one; none popped. Queue depth += 1 (unless already full — see below) |
| 5 |
Operator Stop / abort (499) does not retry and does not insert Continue |
Queue stays as today; Stop still drains after a later successful Ready (unchanged). Only errors are banned from consuming the queue |
| 6 |
Permanent 401/403/validation: no backoff loop; same give-up + Continue-if-queued behavior as exhausted retries |
Don’t hammer auth failures 5 times |
Non-goals / out of scope
- Do not auto-send the Continue item (park it at the head; the operator edits / removes / sends, or a later successful Ready may promote it like any other head)
- Do not persist the queue or Continue on the cloud session (still Wasm-ephemeral)
- Do not raise
MAX_ITEMS 16 / SUBMIT_CAP
- Do not retry Stop
- Do not add a DOM error toast / second composer
- Forbidden wiring: dual DOM chat · secrets in Wasm · laptop-only Production ops
Design
1. Host — retry the in-flight turn (lib/harnessChat.ts / runHarnessTurn)
Stay Busy across attempts. Reuse withTransientRetry (already 5 attempts, abort-aware):
- Retryable: network, 408/429/5xx, timeout, empty model response, other
classifyTurnFailure error that isn’t 401/403/validation/stop.
- Not retryable:
signal.aborted / 499, 401, 403, prompt validation.
- Do not
pushUser on retries (the user line is already on the ring).
- Do not promote or
setLifecycle(Ready) between attempts.
- Backoff: existing defaults (
baseMs 250, capMs 4000) unless a named NEW cap is added (TURN_RETRY_ATTEMPTS = 5, plus base/cap) and listed in AGENTS.md.
Optional in-canvas system line per retry (Retrying 2/5…) — keep it to one line or none; don’t spam five Error rows.
2. Wasm — promote only on success Ready
tryPromoteQueued must not run on busy → err, nor on busy → ready when the turn failed.
Cleanest: host sets Lifecycle.Error on give-up (the enum already exists) and Ready only on success / after operator Stop. Promote stays prev == busy && cur == ready (drop err from terminal for trigger_a). Trigger B (edit closed while idle) unchanged.
Update docs/harness-limits.md When it runs: after the current turn reaches successful Ready, not error.
3. Queue — insert Continue at head, never pop on error
Add submit_queue.insertFront (or pushFront) + bridge wrapper.
On host give-up, if queuedCount() > 0:
- If
count == 16, do not drop operator items; skip the insert (fail closed). Still no promote.
- Else insert the exact prompt:
Continue the current turn
- Same
ITEM_BYTES / normalize path as a normal enqueue (not blank)
The Continue row is a normal queue row (preview, edit, ✕). It is not auto-submitted.
Layer placement
| Concern |
Layer |
Path(s) |
| Retry + backoff |
DOM host |
lib/harnessChat.ts runHarnessTurn (+ withTransientRetry) |
| Lifecycle Error vs Ready |
DOM host + Wasm |
HarnessBridge.setLifecycle · ui.zig promote gate |
| insertFront |
Wasm |
submit_queue.zig · bridge.zig |
| Docs |
living |
docs/harness-limits.md Submit queue · docs/agent-stream.md error · AGENTS.md if new caps |
Testing
| # |
Case |
Layer |
Expected |
| 1 |
Busy + queued + retryable 500, then success on attempt 3 |
host |
One user line; assistant from the successful attempt; queue untouched until success Ready, then current promote-head behavior |
| 2 |
5 retryable failures |
host + Wasm |
Error turn-end; no queue pop; if queue was non-empty, head is Continue the current turn |
| 3 |
5 failures, queue empty |
host |
Stop; no Continue row |
| 4 |
Queue full (16) on give-up |
Wasm |
No insert, no pop |
| 5 |
Operator Stop on attempt 2 |
host |
Immediate abort, no further backoff, no Continue insert; queue intact |
| 6 |
401/403 |
host |
Single attempt, give-up path, no 5× loop |
| 7 |
insertFront unit |
submit_queue host test |
Head is new item; previous head is index 1 |
| 8 |
busy → err does not tryPromoteQueued |
Wasm / ui policy |
Queue count unchanged |
Gates: vitest (harnessChat / host) + zig build test-rich (submit_queue) + zig fmt --check. Agent path is TS+Zig — both layers.
Cloud ops
N/A — no Production mutate. Retry is client-side around the existing /api/agent turn.
Living docs
| Surface |
Change |
docs/harness-limits.md Submit queue |
When it runs = successful Ready only. Error: retry 5× then stop; never promote; Continue-at-head if queue non-empty |
docs/agent-stream.md |
Error / timeout: retries then stop; queue not drained |
AGENTS.md |
New caps if introduced (TURN_RETRY_ATTEMPTS = 5, backoff) |
README / SECURITY / .env.example |
N/A |
Refs submit-queue plan #664 · withTransientRetry · protocol v18 inv_queued_count
Summary
A turn error today looks like a finished turn: the host sets Ready, Wasm treats
busy → ready|erras terminal, andtryPromoteQueuedpops the queue head into a new user send. The failed work is abandoned; the operator’s follow-up runs instead.That is wrong. Errors retry the current task (exponential backoff, 5 attempts) or stop. They never consume queue items.
When retries are exhausted and the queue is non-empty, insert a “Continue the current turn” prompt as the new queue head. Do not pop anything already queued.
Current baseline (live code)
native/harness/src/ui.zig~227–235terminal = ready or err;trigger_a = prev busy and terminal→bridge.tryPromoteQueueddocs/harness-limits.mdSubmit queue → When it runssubmit_queue.promoteIf/bridge.tryPromoteQueuedqueueSubmitFromUi→ poplib/harnessChat.tsrunHarnessChat/ agent failsetLifecycle(Ready)— same as success, so promote firesclassifyTurnFailurestop; timeout / empty / other → errorlib/sandbox/resilience.tswithTransientRetryretriesdefault 4 after the first), exponentialbaseMs/capMs, abort cancels sleep. Used for sandbox attach, not the turnsubmit_queue.zigpushat tail only — no insert-at-0 yet. Cap 16, Wasm-ephemeralOperator report: turn errors with queued follow-ups abort the current message and start the queued one.
Goals
/api/agentfailure (after retries) → queue depth unchanged; no new user row from the old headrawPrompt/ session, no extra user ring lines. Stay Busy for the whole retry window so promote cannot runContinue the current turnas the new headNon-goals / out of scope
MAX_ITEMS16 /SUBMIT_CAPDesign
1. Host — retry the in-flight turn (
lib/harnessChat.ts/runHarnessTurn)Stay Busy across attempts. Reuse
withTransientRetry(already 5 attempts, abort-aware):classifyTurnFailureerror that isn’t 401/403/validation/stop.signal.aborted/ 499, 401, 403, prompt validation.pushUseron retries (the user line is already on the ring).setLifecycle(Ready)between attempts.baseMs250,capMs4000) unless a named NEW cap is added (TURN_RETRY_ATTEMPTS= 5, plus base/cap) and listed in AGENTS.md.Optional in-canvas system line per retry (
Retrying 2/5…) — keep it to one line or none; don’t spam five Error rows.2. Wasm — promote only on success Ready
tryPromoteQueuedmust not run onbusy → err, nor onbusy → readywhen the turn failed.Cleanest: host sets
Lifecycle.Erroron give-up (the enum already exists) and Ready only on success / after operator Stop. Promote staysprev == busy && cur == ready(droperrfromterminalfor trigger_a). Trigger B (edit closed while idle) unchanged.Update
docs/harness-limits.mdWhen it runs: after the current turn reaches successful Ready, not error.3. Queue — insert Continue at head, never pop on error
Add
submit_queue.insertFront(orpushFront) +bridgewrapper.On host give-up, if
queuedCount() > 0:count == 16, do not drop operator items; skip the insert (fail closed). Still no promote.Continue the current turnITEM_BYTES/ normalize path as a normal enqueue (not blank)The Continue row is a normal queue row (preview, edit, ✕). It is not auto-submitted.
Layer placement
lib/harnessChat.tsrunHarnessTurn(+withTransientRetry)HarnessBridge.setLifecycle·ui.zigpromote gatesubmit_queue.zig·bridge.zigdocs/harness-limits.mdSubmit queue ·docs/agent-stream.mderror · AGENTS.md if new capsTesting
Continue the current turninsertFrontunitsubmit_queuehost testbusy → errdoes nottryPromoteQueuedGates: vitest (
harnessChat/ host) +zig build test-rich(submit_queue) +zig fmt --check. Agent path is TS+Zig — both layers.Cloud ops
N/A — no Production mutate. Retry is client-side around the existing
/api/agentturn.Living docs
docs/harness-limits.mdSubmit queuedocs/agent-stream.mdAGENTS.mdTURN_RETRY_ATTEMPTS= 5, backoff).env.exampleRefs submit-queue plan #664 ·
withTransientRetry· protocol v18inv_queued_count