F21 (#815): persisted backend submit queue mirror + reload re-arm + bounded drain - #901
Conversation
…ounded drain backend-agents F21 — persisted backend submit queue + drain, as-built under the operator ship-it override (Wasm untouched, no protocol bump, no new inv_* export). - lib/turnQueue.ts (new): sanitize/append/remove/restore-head/clear helpers for the persisted mirror + reverse-order rearmQueueFromMirror (never double-enqueues; fail-closed on insert reject) + caps: TURN_QUEUE_MAX_ITEMS=16 (Wasm MAX_ITEMS parity), TURN_QUEUE_TEXT_MAX_CHARS=5000 (small prompts are the product case), TURN_QUEUE_DRAIN_MAX_ATTEMPTS=5 (drop-with-paint budget). - SessionSnapshot.queue?: string[] — the mirror rides the EXISTING transcript blob (localStorage JSON locally; transcript-object body on the envelope+Blob carrier). Never a reserved meta key; no new route/server surface. Sanitized fail-closed on every read (trimForCloudPut fold, parseCloudSessionSnapshot restore, local load). - HarnessHost: submit-while-live appends to the mirror; the same runPrompt call's reconcile removes it when the durable start is reached (result.ok OR the failure blended x-workflow-run-id — crash between accept and terminal cannot double-run a drained prompt on reload). Failed starts restore the head (persist + Wasm band queuedInsertFront) and count attempts; give-up drops with a painted Error row. hydrateRingWindow re-arms the FIFO from the mirror after ring rebuilds. - Known residual (documented on the plan issue): Wasm-internal band enqueues are not host-observable without a protocol bump; they drain at runtime but are not crash-safe persisted. Roll-forward one-shot PUT drops unknown body fields, so the cloud mirror rides the envelope+Blob carrier. Gates: vitest default project 2430/2430 green; tsc --noEmit clean. Plan: #815 (backend-agents F21). Parent umbrella: #794.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 6 files · persisted submit-queue mirror + reload re-arm
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Wasm-internal enqueue residual is not a finding (documented operator override on #815). Attack is against the as-built contract this PR actually claims.
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | app/harness/HarnessHost.tsx runPrompt reconcile strips the drained prompt from session.queue only after runHarnessTurn returns. Durable start already happened: lib/harnessChat.ts onTurnStarted → applyCtx.patchSession → onSessionPatch → persistTurn writes turnRunId + running with the in-flight prompt still in the mirror. The PR lock ("keying removal on the durable start, not the terminal") is not implemented. |
1. Host-known follow-up B is in the mirror. 2. Drain-start POST of B is accepted (x-workflow-run-id set, onTurnStarted persisted). 3. F5 before the stream done. 4. hydrateRingWindow re-arms B into the Wasm FIFO; kickColdAttach resumes the live run. 5. That run completes → v19 auto-promote pops B → poll POSTs B again. C15 409 does not apply (first run already finished). Tools/sandbox writes re-run. |
"Removal after result.ok || resultRunId is keying on durable start." That check runs after the terminal of runHarnessTurn, not at header-accept. Mid-turn onSessionPatch already saved the unstripped queue. No test locks "crash between accept and terminal cannot double-run." |
high |
| Major | L1 | Transcript-blob carrier is not copy-forwarded by worker persist. lib/agent/turnPersistSeam.ts buildThisRunChunk emits {id, updatedAt, messages, prev?, depth?} and drops queue. Cloud GET reconstructs from that head (flattenReconstructedBody spreads the worker chunk). mergeAdoptedUsage is { ...server, usage: server.usage ?? local.usage } — server wins. |
1. Cloud session, follow-up B persisted on the host PUT blob. 2. Worker B7 writes a this-run chunk (no queue), advances meta.transcriptPointer. 3. F5. 4. Local first-paint re-arms from localStorage. 5. bootCloudSession onAdopt (fresh heap, inflightRef false) hydrates the worker snapshot without queue and wipes the mirror. Band empty. |
"Host terminal PUT after the turn re-uploads the full snapshot with queue; idle F5 is fine." True after a completed host persist. Mid-turn F5 (the crash this PR exists to survive) GETs the worker head. Overlay-meta is deliberately not the carrier, so envelope adopt cannot save it. |
high |
| Major | L6 | The two reload invariants live only in HarnessHost.tsx (and the worker chunk builder). lib/turnQueue.test.ts proves helpers in isolation; sessionRepository.test.ts proves fold/parse of a blob that already has queue. Nothing proves (a) strip-at-durable-start vs strip-at-terminal, (b) worker copy-forward, (c) adopt does not clobber a local mirror. Claimed lib/sessionStore.test.ts local round-trip is not in the diff. |
Delete the reconcile block (or move it after persistTurn). Helper tests stay green. Double-run and cloud-wipe still ship. |
"HarnessHost is historically source-locked, not unit-rendered." Prior PRs still added *.test.ts source-locks for the invariant (detachTurn.test.ts, turnAttach.test.ts). This PR's safety claims have no equivalent. L6 on the same root as the L1 rows. |
high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Failed-start queuedInsertFront does not auto-promote (Error + promote-gate false); retries are Play / later-success, not a poll tick, despite the drainAttemptsRef comment. queueClear is dead. Duplicate sendWhileRunning texts are skipped via includes while queueAppend allows duplicates — bounded.
Merge guidance
CONCERNS: do not merge until both Majors are fixed:
- Strip the drained prompt from the mirror at durable start (optimistic
removeQueuedText+ persist beforerunHarnessTurnon the non-attach path, restore-head only when the POST never accepted). Mid-turnonSessionPatchmust never carry the in-flight prompt. - Copy-forward
sanitizeQueue(prior.queue)onto worker this-run chunks inbuildThisRunChunkso a B7 pointer advance cannot drop the carrier. - Tests for (1) as a HarnessHost source-lock + (2) as a persist-seam row. Optional: localStorage round-trip in
sessionStore.test.ts(claimed, missing).
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, roll-forward one-shot PUT drop of unknown fields (documented residual #2).
…er chunks Adversarial-review #901 Majors: - HarnessHost: persist removeQueuedText before runHarnessTurn on the non-attach drain path so onTurnStarted/onSessionPatch cannot save the in-flight prompt. Restore-head only when the POST never accepted. Crash between accept and terminal can no longer re-arm a double-POST. - turnPersistSeam buildThisRunChunk: copy-forward sanitizeQueue(prior.queue) so a B7 this-run chunk cannot drop the transcript-blob carrier (cloud adopt was wiping a localStorage re-arm). Tests: HarnessHost source-lock, persist-seam copy-forward, localStorage round-trip. typecheck clean; affected default-project rows green.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 9 files · persisted submit-queue mirror + reload re-arm (HEAD 6e103b2, after the prior-review follow-up)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d (strip-after-terminal, worker drop of queue) landed in 6e103b2. This attack is against HEAD. Wasm-internal enqueue residual is not a finding (documented operator override on #815).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | lib/agent/turnPersistSeam.ts buildThisRunChunk copy-forwards prior.queue verbatim. Host drain-start persist(removeQueuedText) is fire-and-forget coalesced PUT (HarnessHost persist → repo.put; one in-flight PUT per session). lib/workflows/persistStep.ts this-run content is {id, messages} — never queue — so fromContent is always undefined and the worker cannot see the host strip. After a just-completed turn the channel is still inflight with the terminal snapshot that still lists the drained prompt; the strip sits in pending while POST /api/turns starts the next worker. B7 reads the pre-strip blob and writes this-run {queue: [B, C]}. flattenReconstructedBody keeps the head field; mergeAdoptedUsage is {...server, usage: server.usage ?? local.usage} — server wins. |
1. Cloud envelope session. Follow-up B is in the mirror. 2. Turn A terminals; host PUT of {queue:[B,C]} still in flight. 3. Promote drains B; host removeQueuedText is pending behind that PUT. 4. Worker B7 of B copy-forwards [B,C] onto the this-run chunk and advances transcriptPointer. 5. F5. 6. bootCloudSession onAdopt hydrates the worker snapshot with B still queued; hydrateRingWindow re-arms B; kickColdAttach finishes the live run. 7. That run completes → v19 auto-promote pops B → poll POSTs B again. C15 409 does not apply (first run already finished). Tools/sandbox writes re-run. |
"Strip-before-runHarnessTurn plus onTurnStarted persist already saved [C]." Those writes share the same coalesced channel and cannot overtake an in-flight terminal PUT from the previous turn. "Copy-forward was the #901 Major fix." It closed drop; it re-introduced stale include of the in-flight prompt. Local-only F5 is fine (sync writeLocalSession); cloud adopt is the path this PR exists to survive. |
high |
| Major | L6 | The new persist-seam row locks the wrong invariant. copy-forwards session.queue onto worker this-run chunks persists ['follow-up B','follow-up C'] onto a second chunk whose user text is turn-2 user — i.e. copy-forward with no drain of this-run's prompt. Nothing proves: this-run user text matching a mirrored item is removed; a coalesced-PUT race cannot re-arm an in-flight prompt; mergeAdoptedUsage does not clobber a stripped local with a stale server superset. HarnessHost source-lock only checks removeQueuedText appears before runHarnessTurn in the file text. |
Change buildThisRunChunk to strip this-run's first user text from the copy-forwarded queue. Existing F21 rows stay green. Cloud double-run still ships. |
"Helper tests + source-lock were the asked-for L6 of the prior review." They proved the previous patch, not the cloud race this commit left open. Same root as the L1 row. | high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; bounded; inverse of double-run). drainAttemptsRef is in-memory and the comment ("re-promotes on a later poll tick") is false: failed-start queuedInsertFront does not auto-promote (Error + promote-gate false); retries are Play / later success. Roll-forward one-shot PUT still drops unknown body fields (documented residual #2). Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded.
Merge guidance
CONCERNS: do not merge until the Major is fixed:
- Worker this-run chunks must copy-forward
prior.queueminus this-run's first user prompt (removeQueuedTextsemantics).persistStepcontent is{id, messages}— the firstrole:userrow is the drained prompt. A B7 that beats the host strip PUT then writes[C]not[B,C]. - Persist-seam test: prior
{queue:['follow-up B','follow-up C']}, this-run content userfollow-up B→ chunk.queue equals['follow-up C']. Keep the existing omit-queue copy-forward row for the non-matching user-text case.
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host strip PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay).
Adversarial-review #901 HEAD Majors: - turnPersistSeam buildThisRunChunk: copy-forward prior.queue minus this-run's first user text (removeQueuedText semantics). persistStep content is `{id, messages}` so fromContent never carries the host strip, and a coalesced PUT cannot beat B7 — verbatim copy-forward re-armed the in-flight prompt on cloud F5 and double-POSTed after the live run completed. - queueWithoutText helper shared with removeQueuedText. Tests: persist-seam drain-strip + last-item unset; queueWithoutText unit rows. typecheck clean; affected default-project rows green.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 9 files · persisted submit-queue mirror + reload re-arm (HEAD b07397d, after prior-review follow-ups)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d / 6e103b2 (strip-after-terminal, worker drop of queue, verbatim copy-forward of the in-flight prompt) landed in HEAD. This attack is against b07397d. Wasm-internal enqueue residual is not a finding (documented operator override on #815).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | lib/harnessChat.ts preserveQueue = attaching && rawPrompt.trim().length > 0 is false for kickColdAttach (empty prompt, dedup: true). HarnessHost.hydrateRingWindow re-arms the Wasm FIFO from session.queue, then kickColdAttach → runHarnessTurn('', {attach, dedup:true}) → pushSessionToBridge({preserveQueue:false}) → inv_clear_messages wipes the FIFO it just re-armed. Test 2h (F5 cold attach still clears the submit FIFO) locks the wipe. The comment even says kickColdAttach "may clear the (empty) queue" — F21 made that queue non-empty. |
1. Host-known follow-ups B,C are in the mirror; turn A is still running. 2. F5 / switch-back / boot. 3. hydrateRingWindow clearMessages + rearmQueueFromMirror restores [B,C] into Wasm. 4. kickColdAttach microtask cold-attaches with ''. 5. Cold-attach hydrate uses preserveQueue:false → FIFO empty. 6. Attach completes → v19 auto-promote sees an empty band → B/C never drain. Canvas shows no queue chips. Mirror still has them (idle F5 later re-arms) — the reload this PR exists to survive does not restore a drainable FIFO while a turn is live. |
"preserveQueue:false on empty prompt was #857: F5 queue is stale from the previous session." hydrateRingWindow already clearMessages then re-arms this session's mirror; the stale FIFO is gone before kick. #857 test 2h is now a F21 regression lock. "sendWhileRunning already sets preserveQueue true." That path is non-empty prompt; kickColdAttach is the empty-prompt F5 path. Local-only idle F5 (no running turn) is fine (kickColdAttach no-ops). |
high |
| Major | L6 | Same root as L1. lib/turnQueue.test.ts source-lock only asserts removeQueuedText appears before runHarnessTurn. Persist-seam rows lock copy-forward/strip on the blob. Nothing proves: empty-prompt cold attach keeps a just-re-armed FIFO; hydrateRingWindow then kickColdAttach is not a wipe. Test 2h asserts the opposite (expect(exp.__queue).toEqual([])). |
Change preserveQueue to attaching (drop the empty-prompt conjunct). Current F21 helper tests + source-lock stay green. Test 2h fails (it wants the wipe). The F5-running re-arm still ships broken until 2h is inverted. |
"HarnessHost is source-locked, not unit-rendered." This invariant lives in runHarnessTurn (already has a named 2h row) — the lock is present and wrong for F21. L6 on the same root as the L1 row. |
high |
| Minor | L1 | Give-up paints MessageKind.Error via bridge.pushMessage only. runHarnessTurn validation uses appendMessage + push so the Error is in SessionSnapshot.messages. F21 give-up does not, then persistTurn(folded) saves the stripped queue without the row. |
Drain fails 5 times → give-up paints Error, drops the item. F5 rebuilds from snapshot: no Error row, item gone. The "never silent" paint does not survive reload. | "Paint is for the moment of drop, not a durable transcript row." The PR lock is "drop-with-paint, never silent." Other host Errors (validation) persist. Bounded: only the explanation is lost, not a double-run. | high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; inverse of double-run). Worker firstUserText strip is exact-match: a follow-up whose text equals this-run's user line is also dropped from the blob (short prompts like ok / continue). drainAttemptsRef is in-memory and does not auto-promote after a failed start (Error + promote-gate false; retries are Play / later success) despite the "poll tick" comment. Roll-forward one-shot PUT still drops unknown body fields (documented residual #2). Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded. queueClear is unused (Clear/New mint a fresh session).
Merge guidance
CONCERNS: do not merge until the Major is fixed:
runHarnessTurnmustpreserveQueueon every attach, including kickColdAttach's empty prompt. The F5/switch stale-FIFO wipe is alreadyhydrateRingWindow'sclearMessagesbefore re-arm; cold-attach must useinv_clear_ringso the just-re-armed FIFO survives. Invert test 2h: empty-prompt cold attach keeps the FIFO.- Optional Minor:
appendMessagethe give-up Error ontoreconciledbeforepersistTurnso F5 is not silent.
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay), origin roll-forward carrier (BLOB_READ_WRITE_TOKEN still unset).
…rate Adversarial-review #901 HEAD Majors: - runHarnessTurn: preserveQueue on every attach, including kickColdAttach's empty prompt. hydrateRingWindow already clearMessages + re-arms from the persisted mirror; inv_clear_messages on the subsequent cold-attach hydrate wiped that FIFO so a running-turn F5 never restored a drainable band. - Invert test 2h (was an #857 lock of the wipe). - Give-up Error is appendMessage'd onto the snapshot so F5 is not silent. Tests: inverted 2h + source-locks. typecheck clean; affected default-project rows green.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 11 files · persisted submit-queue mirror + reload re-arm (HEAD 5d1787c, after prior-review follow-ups)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d / 6e103b2 / b07397d (strip-after-terminal, worker drop of queue, verbatim copy-forward of the in-flight prompt, kickColdAttach FIFO wipe) landed in HEAD. This attack is against 5d1787c. Wasm-internal enqueue residual is not a finding (documented operator override on #815).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | app/harness/HarnessHost.tsx hydrateRingWindow always clearMessages (wipe FIFO) then rearmQueueFromMirror. That helper is also the live ring snap: Load-earlier and the needSnap path that runs before runPrompt(pending) of a just-promoted head. Promote has already popped the head; the mirror still lists it (strip is inside runPrompt, after this hydrate). Rearm puts the in-flight prompt back into the FIFO. |
1. Host-known follow-ups B,C are in the mirror and the Wasm band. 2. Turn A Ready → v19 auto-promote pops B (pending_submit=B, FIFO [C]). 3. Operator is on a historical ring window (Load earlier) or that Load-earlier just ran so needSnap is true. 4. Poll: hydrateRingWindow wipes [C], rearms from the still-unstripped mirror → FIFO [B,C]. 5. runPrompt(B) strips the mirror to [C] and POSTs B. 6. B completes → auto-promote pops B again → poll POSTs B a second time. C15 409 does not apply (first run already finished). Tools/sandbox writes re-run. Last-item case is the same: promote of sole head leaves FIFO empty, rearm restores it, same double-POST. |
"queuedCount()>0 skips re-arm." After clearMessages the count is 0 — the skip never fires on this path. "Strip-before-runHarnessTurn already dropped B." Strip runs inside runPrompt, after the hydrate. "Load-earlier should rebuild the ring." v21 inv_clear_ring (preserveQueue) is the live-session ring replace; F5/adopt/switch are the cold wipe+rearm. Folding re-arm into a helper used for both is the bug. |
high |
| Major | L6 | Same root as L1. lib/turnQueue.test.ts source-lock only asserts removeQueuedText appears before runHarnessTurn and preserveQueue = attaching. Persist-seam rows lock blob copy-forward. Test 2h locks cold-attach keep. Nothing proves: Load-earlier / needSnap do not re-arm; live ring snaps use preserveQueue (not clearMessages). hydrateRingWindow is one function for cold boot and live paging. |
Change Load-earlier/needSnap to pass a live policy (preserveQueue, no re-arm). Current F21 helper tests + source-lock stay green. The Load-earlier drain double-POST still ships. |
"HarnessHost is source-locked, not unit-rendered." Prior PRs still added named locks for the invariant (detachTurn.test.ts, turnAttach.test.ts, this PR's 2h invert). The new re-arm has no call-site lock. L6 on the same root as the L1 row. |
high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; inverse of double-run). Worker firstUserText strip is exact-match: a follow-up whose text equals this-run's user line is also dropped from the blob (retry-same-prompt / ok / continue). drainAttemptsRef is in-memory, does not auto-promote after a failed start (Error + promote-gate false; retries are Play / later success) despite the comment claiming "re-promotes on a later poll tick". Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded. Roll-forward one-shot PUT still drops unknown body fields (documented residual #2). queueClear is unused (Clear/New mint a fresh session). TURN_QUEUE_MAX_ITEMS is pinned separately from HARNESS_QUEUE_MAX_ITEMS (no equality lock).
Merge guidance
CONCERNS: do not merge until the Major is fixed:
- Split cold vs live hydrate. Cold (boot / adopt / switch):
inv_clear_messages+rearmQueueFromMirror. Live (Load-earlier /needSnapbefore pending submit):inv_clear_ring(preserveQueue: true) and do not re-arm — a just-promoted head is already out of the band and still in the mirror untilrunPromptstrips it. Do not key re-arm onqueuedCount()===0after a live clear: that is exactly the post-promote empty-FIFO case. - Tests:
queueHydratePlan('live')→{preserveQueue:true, rearm:false};queueHydratePlan('cold')→{preserveQueue:false, rearm:true}. Source-lock that Load-earlier and theneedSnaphydrate pass'live'; boot/adopt stay cold.
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay), origin roll-forward carrier (BLOB_READ_WRITE_TOKEN still unset).
Adversarial-review #901 HEAD Majors: - hydrateRingWindow was wipe+rearm for every caller. Load-earlier and the needSnap path before runPrompt(pending) are live ring snaps: promote has already popped the head, the mirror still lists it until drain-start strip, and re-arm put it back into the FIFO so the next Ready double-POSTed it. - Split via queueHydratePlan: cold (boot/adopt/switch) still clearMessages + rearm; live uses inv_clear_ring and never rearms. Do not key re-arm on queuedCount()===0 after a live clear (that is the last-item promote case). Tests: queueHydratePlan unit rows + HarnessHost source-lock for live vs cold call sites. typecheck clean; affected default-project rows green.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 11 files · persisted submit-queue mirror + reload re-arm (HEAD 265d860, after prior-review follow-ups)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d / 6e103b2 / b07397d / 5d1787c (strip-after-terminal, worker drop of queue, verbatim copy-forward of the in-flight prompt, kickColdAttach FIFO wipe, live ring snap re-arm) landed in HEAD. This attack is against 265d860. Wasm-internal enqueue residual is not a finding (documented operator override on #815).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L6 | F21 rewrote app/harness/HarnessHost.tsx runPrompt / hydrateRingWindow call sites and did not update the existing HarnessHost source-locks that pin those strings. lib/detachTurn.test.ts and lib/hostQuotaError.test.ts still assert persistTurn(next, next.turnStatus !== 'running') and hydrateRingWindow(b, session, nextStart); (no 'live'). New F21 rows in lib/turnQueue.test.ts stay green. |
vitest run --project default lib/detachTurn.test.ts lib/hostQuotaError.test.ts → 3 failed (reproduced on HEAD 265d860). merge-pr full vitest gate is red. PR cannot merge. |
"Rename-only; the #870 paint-skip invariant still holds as persistTurn(reconciled, …)." The production skip is intact. The lock is not — merge-pr / npm test is the contract, and the claimed default-project green is false. Leaving the #870 locks pointing at a dead next means a later paint-skip regression on the epoch path would not fail those rows. The Load-earlier regex no longer proves paintQuotaAfterRebuild still follows the live hydrate. |
high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; inverse of double-run). Worker firstUserText strip is exact-match: a follow-up whose text equals this-run's user line is also dropped from the blob. drainAttemptsRef is in-memory; the comment ("re-promotes on a later poll tick") is still false — failed-start setFailLifecycle arms promote-gate false + Error, so retries are Play / later success. setFailLifecycle may insert CONTINUE_TURN_PROMPT when siblings remain in the FIFO (pre-existing #759; F21 then queuedInsertFronts the restored head in front of it). Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded. Roll-forward one-shot PUT still drops unknown body fields (documented residual #2). queueClear is unused. TURN_QUEUE_MAX_ITEMS is pinned separately from HARNESS_QUEUE_MAX_ITEMS (no equality lock).
Merge guidance
CONCERNS: do not merge until the Major is fixed:
- Update the existing HarnessHost source-locks to the F21 symbols:
persistTurn(reconciled, reconciled.turnStatus !== 'running')inlib/detachTurn.test.tsandlib/hostQuotaError.test.ts; Load-earlier / needSnap regexes must allowhydrateRingWindow(..., 'live')and still require the followingpaintQuotaAfterRebuild(the #870 wipe-then-paint invariant). - Re-run those two files (not only
lib/turnQueue.test.ts) so default-project is actually green.
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay), origin roll-forward carrier (BLOB_READ_WRITE_TOKEN still unset).
Adversarial-review #901 HEAD Major: - persistTurn(next, ...) became persistTurn(reconciled, ...) on the epoch path; detachTurn + hostQuotaError source-locks still asserted the dead `next`. - Load-earlier / needSnap hydrates now pass kind 'live'; the #870 wipe-then-paint regex still required hydrateRingWindow(... nextStart); with no extra arg. The production paint-skip / paintQuotaAfterRebuild invariants were intact; the locks were not. Default-project rows were red (3 failed). Tests: updated locks. typecheck N/A (tests only).
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 13 files · persisted submit-queue mirror + reload re-arm (HEAD 06613f4, after prior-review follow-ups)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d / 6e103b2 / b07397d / 5d1787c / 265d860 (strip-after-terminal, worker drop of queue, verbatim copy-forward of the in-flight prompt, kickColdAttach FIFO wipe, live ring snap re-arm, stale #870 source-locks) landed in HEAD. This attack is against 06613f4. Wasm-internal enqueue residual is not a finding (documented operator override on #815). Roll-forward PUT drop of unknown fields is not a finding (documented residual #2; same-id boot LWW keeps a newer-or-equal local mirror).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | lib/agent/turnPersistSeam.ts firstUserText exact-matches the this-run user row against session.queue. Production persistStep content is checkpointToSnapshotMessages(fold.checkpoint), and that checkpoint's user content is turnWorkflow userMessage = POST /api/turns parsed.prompt = host formatPromptWithHistory(session.messages, prompt) (lib/harnessChat.ts apiPrompt → sendTurnStream). After the first turn, that string is the folded blob (Previous conversation…\nUser: …\nUser: follow-up B\n\nAssistant:), not the raw queue item. queueWithoutText no-ops; copy-forward keeps the in-flight prompt on the worker head. Worker updatedAt is newer than the host strip PUT → shouldAdoptServer adopts the worker snapshot. |
1. Cloud envelope session. Turn A completes; host-known follow-up B is in the mirror. 2. Drain POSTs B; host removeQueuedText persists [C]. 3. Workflow userMessage is the history fold, not B. B7 copy-forwards [B,C] onto the this-run chunk and advances transcriptPointer. 4. F5. 5. bootCloudSession onAdopt hydrates the worker snapshot with B still queued; hydrateRingWindow re-arms B; kickColdAttach finishes the live run. 6. That run completes → v19 auto-promote pops B → poll POSTs B again. C15 409 does not apply (first run already finished). Tools/sandbox writes re-run. |
"The persist-seam row already strips when this-run user is follow-up B." That is not the production shape — runHarnessTurn sends apiPrompt (folded) on every drain after the first turn, and a queue implies prior history. "Host strip-before-runHarnessTurn already saved [C]." Worker overlay clock is later; same-id adopt is server-wins on updatedAt. "firstUserText was the #901 HEAD Major fix." It closed bare-prompt copy-forward; every real follow-up drain is a fold. |
high |
| Major | L6 | Same root as L1. copy-forward drops this-run user prompt from the queue persists a this-run user row whose text equals the queued item. Nothing proves: a formatPromptWithHistory user row still strips B; a coalesced-PUT race cannot re-arm an in-flight prompt when the checkpoint user is the fold. Helper queueWithoutText unit rows stay green either way. |
Change firstUserText to extract the last User: line of a history fold (or pass the raw prompt into persist). Existing F21 persist-seam rows stay green. Cloud double-run still ships. |
"Helper tests + source-lock were the asked-for L6 of the prior review." They proved the previous patch against a synthetic {role:user,text:'follow-up B'} body, not the apiPrompt the host actually POSTs. L6 on the same root as the L1 row. |
high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; inverse of double-run). drainAttemptsRef is in-memory; the comment ("re-promotes on a later poll tick") is still false — failed-start setFailLifecycle arms promote-gate false + Error, so retries are Play / later success. setFailLifecycle may insert CONTINUE_TURN_PROMPT when siblings remain in the FIFO (pre-existing #759; F21 then queuedInsertFronts the restored head in front of it). Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded. Roll-forward one-shot PUT still drops unknown body fields (documented residual #2). queueClear is unused. TURN_QUEUE_MAX_ITEMS is pinned separately from HARNESS_QUEUE_MAX_ITEMS (no equality lock). docs/session-model.md blob shape still omits queue and says extra keys are ignored.
Merge guidance
CONCERNS: do not merge until the Major is fixed:
- Worker this-run chunks must strip the raw drained prompt, not the history-folded
userMessage.firstUserTextshould treat aformatPromptWithHistoryblob as "lastUser:line before the trailingAssistant:" (bare prompt stays exact-match). A B7 of follow-upBthen writes[C], not[B,C]. - Persist-seam test: prior
{queue:['follow-up B','follow-up C']}, this-run content user =formatPromptWithHistory([turn-1 user, turn-1 assistant], 'follow-up B')→ chunk.queue equals['follow-up C']. Keep the existing bare-prompt strip row.
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay), origin roll-forward carrier (BLOB_READ_WRITE_TOKEN still unset).
… strip Adversarial-review #901 HEAD Majors: - turnPersistSeam firstUserText exact-matched the this-run user row against session.queue. Production persistStep content is the workflow userMessage, which runHarnessTurn POSTs as formatPromptWithHistory (apiPrompt) — after the first turn that is the folded blob, not the raw queue item. Copy-forward kept the in-flight prompt on the worker head; cloud F5 adopted it and double-POSTed after the live run completed. - queueTextFromUserContent takes the last User: line of a history fold (bare prompts pass through). Persist-seam row uses a real fold. Tests: queueTextFromUserContent unit rows + persist-seam folded-userMessage strip. typecheck clean; affected default-project rows green.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: CONCERNS
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 13 files · persisted submit-queue mirror + reload re-arm (HEAD fca3b10, after prior-review follow-ups)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d / 6e103b2 / b07397d / 5d1787c / 265d860 / 06613f4 (strip-after-terminal, worker drop of queue, verbatim copy-forward of the in-flight prompt, kickColdAttach FIFO wipe, live ring snap re-arm, stale #870 source-locks, history-fold userMessage unwrap) landed in HEAD. This attack is against fca3b10. Wasm-internal enqueue residual is not a finding (documented operator override on #815). Roll-forward PUT drop of unknown fields is not a finding (documented residual #2).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Major | L1 | mergeAdoptedUsage is {...server, usage: server.usage ?? local.usage} — whole-snapshot server-wins except usage. F21's host-known persist path is queueAppend (send-while-running) then a coalesced repo.put. Worker B7 of the live turn copy-forwards prior.queue from before that PUT lands, stamps a newer overlay updatedAt (max(Date.now(), stored+1)), and advances transcriptPointer. Boot/F5 shouldAdoptBootServer + mergeAdoptedUsage hydrates the worker snapshot without the just-appended follow-up. Inverse of the drain-strip race: lost prompt, not double-run. onAdopt skipping while inflightRef is true does not help — F5 is a fresh heap. |
1. Envelope session. Turn A running; host inflight false (503 / D18). 2. Operator Send B; queueAppend writes local [B] (updatedAt=T1) and coalesces a PUT. 3. Worker B7 of A reads prior blob (PUT not landed / older pointer), copy-forwards empty-or-stale queue (no B), writes this-run chunk, overlay clock T2>T1. 4. F5. 5. localStorage first-paint has [B]; bootCloudSession onAdopt LWW-adopts the worker snapshot; hydrateRingWindow rearms empty. B never drains. |
"queueAppend PUT lands and becomes prior, so the next B7 copy-forwards [B]." Only if the PUT beats B7. Mid-turn B7 is on every tool batch; overlay clock is monotonically newer than the append. Previous #901 Majors already proved coalesced host PUTs lose this race in the other direction. "mergeAdoptedUsage is usage-only by design." That is why a new transcript-body field needs the same field-level merge usage got — server-wins on the whole snapshot is how a missing/stale queue clobbers local. "Local-only F5 is fine." True; this PR's cloud adopt path is the one it claims to survive. |
high |
| Major | L6 | Same root as L1. Persist-seam rows lock copy-forward and this-run strip on the worker chunk. mergeAdoptedUsage tests only usage. Nothing proves: same-id adopt of a worker head that omitted a local queueAppend keeps B; same-id adopt of a stale-long server queue still strips the in-flight last user so B cannot re-arm. Helper sanitizeQueue / queueAppend rows stay green either way. |
Change mergeAdoptedUsage to keep local extras (union) and strip queueTextFromUserContent(last user) of the adopted messages. Existing F21 rows stay green. Cloud F5 still drops a host-known follow-up that lost the PUT race. |
"Helper tests + source-lock were the asked-for L6 of prior reviews." They proved strip/copy-forward on the worker blob, not the adopt merge that actually hydrates F5. L6 on the same root as the L1 row. | high |
| Minor | L6 | TURN_QUEUE_MAX_ITEMS is pinned separately from HARNESS_QUEUE_MAX_ITEMS (both 16) with no equality lock. Drift → rearm of a 17th Wasm item is dropped by sanitize, or a 16-cap mirror cannot restore a raised Wasm FIFO. |
Change one constant. F21 helper tests stay green. Reload re-arm silently truncates. | "Comment says pinned to avoid a cycle." A test-only import of both constants is cycle-free and is how other dual-pins are locked. | high |
| Minor | L8 | docs/session-model.md still describes the Blob head as {id, updatedAt, messages, prev?, depth?} and says "Extra keys are ignored." F21's carrier is an extra key (queue). A later persist change that believes the doc will drop the mirror (the original #901 worker-drop Major). |
New-hire / agent implements flatten/persist from the doc, omits queue. Cloud adopt wipes the mirror again. |
"Code + tests are the contract." Living-docs rule in AGENTS.md: docs/session-model.md is the session restore truth. Residual listed on the last review, still unfixed. |
high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; inverse of double-run). drainAttemptsRef is in-memory; the comment ("re-promotes on a later poll tick") is still false — failed-start setFailLifecycle arms promote-gate false + Error, so retries are Play / later success. setFailLifecycle may insert CONTINUE_TURN_PROMPT when siblings remain in the FIFO (pre-existing #759; F21 then queuedInsertFronts the restored head in front of it). Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded. Roll-forward one-shot PUT still drops unknown body fields (documented residual #2). queueClear is unused (Clear/New mint a fresh session). queueTextFromUserContent takes the last \nUser: line, so a follow-up whose body itself contains that marker can fail the worker strip (paste-a-transcript; bounded).
Merge guidance
CONCERNS: do not merge until the Major is fixed:
- Same-id adopt must field-merge
queue, not whole-snapshot server-wins. Union server+local (server order, then local copies not already present, capTURN_QUEUE_MAX_ITEMS), thenqueueWithoutTextofqueueTextFromUserContent(last user row on the adopted messages)so a stale-long server queue cannot re-arm an in-flight prompt and a worker head that missedqueueAppendcannot drop it. - Tests on
mergeAdoptedUsage: (a) serverqueueomitted, local['follow-up B']→['follow-up B']; (b) server['follow-up B','follow-up C'], local['follow-up C'], last user is aformatPromptWithHistoryfold offollow-up B→['follow-up C']. Keep existing usage rows.
Minors: lock TURN_QUEUE_MAX_ITEMS === HARNESS_QUEUE_MAX_ITEMS; name queue on the Blob shape in docs/session-model.md (stop saying extra keys are ignored).
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay), origin roll-forward carrier (BLOB_READ_WRITE_TOKEN still unset / NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE default off).
…rip in-flight) Adversarial-review #901 HEAD Majors: - mergeAdoptedUsage was whole-snapshot server-wins except usage. Worker B7 of the live turn copy-forwards prior.queue from before a coalesced queueAppend PUT lands, stamps a newer overlay clock, and boot/F5 adopt wiped the host-known follow-up (inverse of the drain-strip race). - Union server+local (server order, then local copies not already present), then queueWithoutText of queueTextFromUserContent(last user) so a stale-long server queue cannot re-arm an in-flight drain. Minors: lock TURN_QUEUE_MAX_ITEMS === HARNESS_QUEUE_MAX_ITEMS; name queue on the Blob shape in docs/session-model.md; drainAttemptsRef comment no longer claims poll-tick auto-promote. Tests: mergeQueues unit rows + mergeAdoptedUsage adopt rows + source-lock. typecheck clean; affected default-project rows green.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #901
Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: main ← f21/persisted-queue-drain · 14 files · persisted submit-queue mirror + reload re-arm (HEAD 3ea0958, after prior-review follow-ups)
Lenses run: L1, L2, L3, L5, L6, L8 (skip L4/L7/L9: no CI/workflow/deploy, no reusability bind, no palette/layout)
AGENTS.md read: yes (feature-divide yes — host/bridge/agent loop; SECURITY.md N/A — no workflow/secret/runner/API route)
Prior-review Majors on 400df2d / 6e103b2 / b07397d / 5d1787c / 265d860 / 06613f4 / fca3b10 (strip-after-terminal, worker drop of queue, verbatim copy-forward of the in-flight prompt, kickColdAttach FIFO wipe, live ring snap re-arm, stale #870 source-locks, history-fold userMessage unwrap, whole-snapshot server-wins adopt) landed in HEAD. This attack is against 3ea0958. Wasm-internal enqueue residual is not a finding (documented operator override on #815). Roll-forward PUT drop of unknown fields is not a finding (documented residual #2).
Affected default-project files re-run on HEAD: 459/459 green (turnQueue, sessionRepository, sessionStore, turnPersistSeam, detachTurn, hostQuotaError, harnessChat).
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Minor | L6 | Production cloud GET is reconstructTranscriptChain → flattenReconstructedBody (spreads headBody) → parseCloudSessionSnapshot. F21 locks copy-forward on the raw worker chunk and mergeAdoptedUsage on already-parsed snapshots. Nothing proves the GET flatten keeps queue. |
Change flattenReconstructedBody to copy only {id, updatedAt, messages} (the pre-F21 documented shape). Persist-seam + merge rows stay green. New-device / empty-local ?s= pin adopts a snapshot with no queue; hydrateRingWindow rearms empty; host-known follow-ups never drain. Same-id local extras still save a same-heap F5 — the miss is cloud-only restore. |
"Spread keeps extra keys today." True on HEAD. The GET path is the only remaining untested carrier hop; a whitelist flatten is the original worker-drop Major in a different function. | high |
| Minor | L8 | lib/sessionRepository.ts trimForCloudPut still comments Empty mirror omits the field (absent = clear on adopt-restore). HEAD field-merge is union + strip (mergeQueues); omit is not a clear. |
New-hire / agent treats omit-as-clear, reverts mergeAdoptedUsage to whole-snapshot server-wins or skips the union. A coalesced queueAppend that lost the B7 race is wiped on F5 again. |
"Code + merge tests are the contract." Living-docs / in-code comments are how the last worker-drop Major was supposed to stay dead. docs/session-model.md was updated; this comment was not. |
high |
Residual risk
Host-known-only persist still means composer-while-Busy (enqueueFromUi) dies on F5 — accepted residual. Optimistic strip-before-POST can lose a queued prompt on F5 in the strip-persist-but-POST-never-accepted window (local; inverse of double-run). mergeQueues / worker firstUserText exact-match strip also drops a follow-up whose text equals the live last user (ok / continue / retry-same-prompt). drainAttemptsRef is in-memory (reload starts a fresh 5). setFailLifecycle may insert CONTINUE_TURN_PROMPT when siblings remain; F21 then queuedInsertFronts the restored head in front of it (pre-existing #759). Duplicate sendWhileRunning texts skipped via includes while queueAppend allows duplicates — bounded. queueTextFromUserContent takes the last \nUser: line, so a follow-up that itself contains that marker can fail the worker strip (paste-a-transcript; bounded). queueClear is unused (Clear/New mint a fresh session).
Merge guidance
PASS WITH NOTES: the double-run / lost-queueAppend family did not breach on 3ea0958 with current evidence. Minors should land before or with merge — they are the two remaining untested/stale-comment hops on the carrier this PR exists to keep.
What was not attacked
Live DO runner, prod Gateway, Wasm FIFO internals (untouched by design), /api/turns gates, protocol v18–v21, envelope LWW of a late host PUT vs worker this-run messages (pre-existing host/worker persist race; B8 overlay), origin roll-forward carrier (BLOB_READ_WRITE_TOKEN still unset / NEXT_PUBLIC_HARNESS_CARRIER_ENVELOPE default off).
Adversarial-review #901 HEAD Minors: - flattenReconstructedBody spreads the worker head; a whitelist flatten to {id, updatedAt, messages} would drop the F21 carrier on cloud GET (new-device / empty-local ?s= pin) while persist-seam and merge rows stayed green. - trimForCloudPut comment claimed omit = clear on adopt-restore; same-id adopt field-merges via mergeQueues. Tests: flatten keeps queue; GET flatten+parse restores it. typecheck N/A (comment + tests).
backend-agents F21 — persisted backend submit queue + drain
Plan #815 · parent umbrella #794 · branch
f21/persisted-queue-drainShips F21 as-built under the operator's ship-it directive (2026-08-30): the Wasm/protocol surface is untouched — no new
inv_*export, no Zig change, no cap change. The runtime Wasm submit FIFO (v18 depth / v20 insert-front / v21 preserve / promote gate) stays exactly as-is; this PR adds what the host can own without touching it: the persisted mirror, reload re-arm, and a bounded drop budget for failed drain starts. The plan's enqueue-visibility question is resolved as a documented residual on #815 (living comment), not a gate.What landed
lib/turnQueue.ts(new)sanitizeQueue(fail-closed: drop blanks/over-cap items, depth cap, empty = unset),queueAppend,removeQueuedText(first-match),queueRestoreHead(defer path),queueClear,rearmQueueFromMirror(reverse-order v20 inserts, only when the Wasm queue is empty, stop on any insert reject)SessionSnapshot.queue?: string[]trimForCloudPutfolds it,parseCloudSessionSnapshotrestores it sanitized,overlayEnvelopeMetadeliberately leaves it alone — meta is not its carrier). Never a reservedmetakey, no new route, no new server surfaceapp/harness/HarnessHost.tsxrunPromptresult.okOR the failure blendedx-workflow-run-id(the post-headers accepted-POST blend). Keying removal on the durable start (not the terminal) means a crash between accept and terminal can never double-run a drained prompt on reloadqueueRestoreHeadpersist + WasmqueuedInsertFront) and count a failed attempt; give-up atTURN_QUEUE_DRAIN_MAX_ATTEMPTS= 5 drops the item with a painted Error row, never silenthydrateRingWindowqueuedInsertFront); skips when the Wasm queue is non-empty (never double-enqueues); fail-closed on insert reject (items stay in the mirror).inv_queued_countparity holds; removal keying makes reload-never-double-STARTS hold toolib/turnQueue.tsTURN_QUEUE_MAX_ITEMS= 16 (exact parity with Wasmsubmit_queue.MAX_ITEMS),TURN_QUEUE_TEXT_MAX_CHARS= 5,000 (the queue is for small follow-up prompts — big prompts belong on the transcript per the #794 post-mortem),TURN_QUEUE_DRAIN_MAX_ATTEMPTS= 5 (new, generous; no existing cap raised/lowered)Explicitly not changed
native/harness/**(Wasm/Zig), bridge protocol, allinv_*exports./api/turnsroute and its 401/400/429/409/403/503 gates; drain still POSTs through the samerunPrompt→runHarnessTurn→sendTurnStreamfunnel the composer uses (≤1 child in flight by construction — the drain IS a normal host submit).queue_paused/promote-gate semantics.Residuals (documented on #815, not blockers)
enqueueFromUi, band edit/remove/Clear) are not host-observable without an additive export (protocol v22); they drain at runtime but are not crash-safe persisted. Fix path (one additive REQUIRED export, v18/v19/v20/v21 precedent) is written up on the living comment.PUT /api/sessions/:iddrops unknown body fields, so the cloud mirror rides the envelope+Blob carrier (and localStorage), not the roll-forward record.Gates
vitestdefault project: 2430/2430 green (includes the newlib/turnQueue.test.ts+ F21 rows inlib/sessionRepository.test.ts).npm run typecheck: clean.Closes #815 (with the enqueue-visibility residual recorded on the issue for a future protocol-v22 follow-up).