Skip to content

feat(harness): turn errors retry the current turn — never drain the queue (plan #759) - #774

Merged
btipling merged 5 commits into
mainfrom
plan/turn-retry-never-drain-queue
Aug 22, 2026
Merged

feat(harness): turn errors retry the current turn — never drain the queue (plan #759)#774
btipling merged 5 commits into
mainfrom
plan/turn-retry-never-drain-queue

Conversation

@btipling

Copy link
Copy Markdown
Owner

Closes #759

A turn error no longer looks like a finished turn: the host retries the SAME prompt up to TURN_RETRY_ATTEMPTS=5 against a retryable error with bounded exponential backoff, stays Busy the whole window, then lands the turn on Error — so the Wasm promote gate (Ready-only) never drains a queued operator item. On give-up with a non-empty queue it inserts Continue the current turn as the new head (never when full).

Host (TS)lib/harnessChat.ts wraps the /api/agent send in withTransientRetry via an additive classify seam (lib/sandbox/resilience.ts, default classifyVercelError, sandbox untouched); narrow classifyTurnRetry = permanent on stop/401/403/other 4xx, retryable on timeout/empty/5xx/network. Give-up sets Lifecycle.Error (Stop keeps Ready); Continue-insert fail-closed at HARNESS_QUEUE_MAX_ITEMS=16.

Wasm (Zig)submit_queue.insertFront, bridge.insertQueuedFront + inv_queued_insert_front, listed in build.zig export_symbol_names; ui.zig promote gate is now terminal = cur_lc == .ready (err is no longer terminal). Protocol v18→v19 with the REQUIRED export (note: sibling #760 also bumps to v19 — per the plan's union rule the second merged takes v20).

Tests: 8 new/updated host retry cases (plan tests 1,2,3,5,6,10 + full-queue fail-closed), classifier-seam back-compat in resilience.test.ts (test 11), queuedInsertFront bridge rows + REQUIRED (test 12), 3 new wasm-int v19 rows (artifact proves the export is rooted), and 5 new submit_queue Zig tests incl. wrap-around + promote-after-insert.

Gates green: npm run typecheck, node scripts/di-gate.mjs, full vitest run (1963/1963, 126 files incl. wasm-int), npm run build, zig build harness -Doptimize=Debug, zig build test-rich, zig build test-rich-invariants, zig fmt --check. The 4 dvui_testing layout "failed command" lines are the pre-existing missing-font artifacts (byte-identical on clean main).

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
invincible Ignored Ignored Aug 22, 2026 7:23am

Request Review

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #774

Verdict: CONCERNS
Repo: btipling/invincible
Scope: mainplan/turn-retry-never-drain-queue · 17 files · protocol v19 inv_queued_insert_front + turn retry (plan #759)
HEAD: 4e859e42010eca9eefab922fdce756e388ce168d
Lenses run: L1, L2, L3, L4, L6, L8 (skip: L5 no new poll/history; L7 no clone bind; L9 no palette)
AGENTS.md read: yes · docs/feature-divide.md not changed (host still owns /api/agent + lifecycle; Wasm still owns the FIFO) — divide holds

The shape is right: host is the sole outcome observer, withTransientRetry + injectable classify leaves sandbox callers on classifyVercelError, Stop is permanent / Ready / no Continue, give-up lands Lifecycle.Error so ui.zig terminal = cur_lc == .ready cannot drain, insertFront fails closed at 16, export is in build.zig export_symbol_names, build-harness green, JSON-path tests cover attempts/Continue/full/Stop/401. Feature-divide one-liner: a multi-turn chat still completes from the Wasm composer; the host only arms lifecycle + unshifts Continue.

Two independent holes in the new seams.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Major L1 withTransientRetry wraps the live sendAgentStream call with the same onStreamEvent closure. assistantAcc / assistantStarted / live tool cards / next.attachedSlugs are not reset between attempts. Production default is streamAgent === true. Every new test forces streamAgent: false. Attempt 1 streams tool_start + text_delta, then a retryable 5xx/timeout/error event. Attempts 2–5 POST the same prompt again: tools re-run (writes/exec duplicate), growAssistant pushes another bubble, skill rows restamp. Canvas and sandbox diverge from “retry the current turn”. Fail-closed: if any tool_start (or any ring mutation past the user line) already painted, classify remaining failures permanent — or snapshot/reset the stream accumulators before attempt N+1. Defender: “goal 2 is retry the same prompt; JSON tests prove 5 attempts.” Survives: JSON never paints live. The issue’s production path is SSE. Defender: “the plan wanted 5×.” Retrying a turn is not the same as replaying a half-painted stream onto the same ring. high
Major L1 inv_queued_insert_frontsubmit_queue.insertFront shifts every slot but does not touch state.queue_editing_index. queue_band.removeAt already does if (e > i) e - 1 for the same class of shift. Host give-up can fire while Busy-queue-edit is open (that is the queue’s purpose). Queue [A, B], operator is editing B (index = 1). Turn gives up → Continue unshifted → [Continue, A, B]. Editor is still index 1 (now A). Blur/Ctrl+Enter saveEdit overwrites A with B’s buffer; B’s unsaved text is gone. Fix: on successful insert-front, if queue_editing_index != null increment it (or cancel the edit). Skip insert while editing is the fail-closed alternative. Defender: “who edits during a failing turn?” The FIFO exists specifically to edit follow-ups while Busy; backoff is ~seconds (250 doubling to 4000). Defender: “CloseEdit on empty FIFO.” Count went up, not down — the latch stays. high
Minor L6 Plan tests 1/2/3/5/6/10 all run through sendAgent + streamAgent: false. No stream-retry case, no insert-front-while-editing case. The two Majors above are exactly the untested branches. CI stays 1963/1963 while the production SSE path duplicates tools. Defender: “wasm-int proves the export.” It proves the symbol exists, not the retry×stream interaction. high
Nit L8 docs/harness-limits.md Permanent-failures row claims validation gets Continue-if-queued. runHarnessTurn validatePrompt early-return still setLifecycle(Ready) and never calls insertContinueTurnPrompt. Client-side validation is pre-Busy, so it does not drain — the Continue claim is still wrong. runHarnessChat header comment still says “soft API errors leave ready for retry.” Docs/code drift only. Defender: “validation never starts a turn.” Then don’t document it as the give-up+Continue path. high

Residual risk

Sibling #763 (plan #760) also ships protocol v19 with a different REQUIRED export (inv_set_queue_promote_allowed). Whichever merges second must take v20 and keep both exports in REQUIRED_FNS / export_symbol_names — the plan said this; the PR bodies have not sequenced it. This PR alone still auto-drains the queue (incl. a parked Continue) on a later successful Ready after Stop — that is goal 5 as locked, not a defect. insertFront’s 256 KiB stack scratch matches pre-existing replaceAt; not new. Live operator smoke of a streamed 5xx-after-tools was not run in this attack.

Merge guidance

CONCERNS: do not merge until the stream-retry accumulator is isolated (or retries are suppressed once the stream has painted) and insert-front adjusts or cancels queue_editing_index. The L6 stream test should land in the same pass. Protocol-union with #763 is a merge-time must, not a reason to land this first unchecked.

What was not attacked

Live DO runner beyond green build-harness; prod Gateway tool replay; in-browser Stop-during-backoff smoke; thinking-collapse on busy→err (existing unit already covers that transition).

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #774 (round 2)

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/turn-retry-never-drain-queue · 20 files · protocol v19 inv_queued_insert_front + turn retry (plan #759)
HEAD: 8c61c1fc9b18073cf350391b5d7a8c319f6f276c (round-1 follow-up on 4e859e4)
Lenses run: L1, L2, L3, L4, L6, L8 (skip: L5 no new poll/history; L7 no clone bind; L9 no palette)
AGENTS.md read: yes · docs/feature-divide.md not changed (host owns /api/agent + lifecycle; Wasm owns the FIFO)

Round 1 (CONCERNS on 4e859e4) asked for two Majors, an L6 Minor, and a docs Nit. This head landed all four:

  • Stream replay: streamPainted is armed on every ring-painting SSE type (tool_start / tool_result / reasoning_delta / text_delta / skill_attached). withTransientRetry’s classify is then streamPainted ? permanent : classifyTurnRetry. sendAgentStream awaits onEvent before returning, so the flag is set before classify. JSON path never fires onStreamEvent — 5× loop unchanged there. Tests: painted 500 → 1 send, one tool + one assistant; unpainted 503 → 5 sends.
  • Edit re-anchor: insertQueuedFront arms has_pending_front_insert; queue_band.reconcileFrontInsert (called from ui.frame after the empty-queue drop) consumes it and bumps queue_editing_index by one. Failed insert does not arm. reset / inv_clear_messages clear the latch. Zig tests cover bump / no-op / consume / reset.
  • Docs: Permanent-failures row no longer claims client-side validation inserts Continue.

Give-up still lands Lifecycle.Error so terminal = cur_lc == .ready cannot drain. Stop stays Ready / no Continue / no retry. insertFront fails closed at 16. Export is in export_symbol_names. build-harness green on this SHA (workflow_dispatch 32548049169). Feature-divide holds.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Nit L1 Front-insert latch is a boolean, not a count. reconcileFrontInsert bumps +1 once per consumed signal. Two successful insertQueuedFront calls before the next Wasm frame would shift twice and bump once — saveEdit would still land on the neighbor. Production give-up inserts Continue once. No second caller today. A counter (or bump-by-queuedCount delta) would close it if a second host unshift appears. Defender: “one Continue per give-up.” Correct for this PR. high

Residual risk

GitHub mergeable_state: dirty — this branch conflicts with main (#773 Ctrl+I landed; likely docs/harness-limits.md + .gitignore .invincible). Rebase before merge; not a logic hole in the retry/FIFO seams. Sibling #763 is still open and also ships protocol v19 with a different REQUIRED export (inv_set_queue_promote_allowed). Whichever merges second must take v20 and keep both exports. This PR alone still auto-drains the queue (incl. a parked Continue) on a later successful Ready after Stop — that is goal 5 as locked. usage SSE does not arm streamPainted (status-slot only, no ring paint) — a usage-then-5xx still retries; acceptable. Live operator smoke of a streamed 5xx-after-tools was not run in this attack.

Merge guidance

PASS WITH NOTES. Round-1 Majors are fixed; this is safe to merge after a rebase onto main and the protocol-union with #763 (v20 + both exports if that PR lands first). The leftover Nit is a second-unshift counter, not a drain/security hole.

What was not attacked

Live DO runner beyond green build-harness; prod Gateway tool replay; in-browser Stop-during-backoff; thinking-collapse on busy→err (existing unit already covers that transition).

@btipling
btipling force-pushed the plan/turn-retry-never-drain-queue branch from 8c61c1f to a797a1f Compare August 22, 2026 05:38

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #774 (round 3)

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/turn-retry-never-drain-queue · 19 files · protocol v20 inv_queued_insert_front + turn retry (plan #759); sibling #760 already on main as v19
HEAD: a797a1f20c4757e268b941b9f0d6736d4ff4019e
Lenses run: L1, L2, L3, L4, L6, L8 (skip: L5 no new poll/history growth; L7 no clone bind; L9 no palette)
AGENTS.md read: yes · docs/feature-divide.md not changed (host still owns /api/agent + lifecycle; Wasm still owns the FIFO) — divide holds

Round 1 (CONCERNS) required stream-retry isolation and insert-front edit re-anchor. Round 2 (PASS WITH NOTES on 8c61c1f) landed those. This head is the protocol-union rebase: #760 took v19, this sibling is v20 with both REQUIRED exports. Re-traced the production seams on this SHA, not the prior comments.

The shape still holds: withTransientRetry + additive classify (sandbox default untouched), streamPainted fail-closes retries once SSE has painted past the user line, give-up lands Lifecycle.Error so shouldAutoPromote (ready-only) cannot drain, Stop stays Ready / setQueuePromoteAllowed(false) / no Continue, insertFront fails closed at 16, export is in build.zig export_symbol_names and REQUIRED_FNS next to inv_set_queue_promote_allowed. Feature-divide one-liner: a multi-turn chat still completes from the Wasm composer; the host only arms lifecycle + unshifts Continue.

No Blocker or Major survived self-refutation on this SHA.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Minor L8 native/harness/README.md still documents protocol 19 and labels inv_queued_insert_front as v19. This PR edited those exact rows on the union rebase and left them one version behind the code. inv_set_queue_promote_allowed is still absent from the export table. Operator / next protocol bump treats README as the contract: HARNESS_PROTOCOL_VERSION / PROTOCOL_VERSION / wasm-int are 20 (lib/harnessBridge.ts · bridge.zig). Shipping a “fix README” wasm at 19 against this host fails closed with Harness protocol mismatch: wasm=19 host=20. Inverse: someone “fixes” the host down to 19 and drops the v19 promote-gate export. Defender: “AGENTS.md + REQUIRED_FNS + wasm-int pin v20; README is secondary.” Survives: this file is the crate’s protocol table, this PR is the one that rewrote the version line, and the union rule the plan locked is exactly “don’t lose the other export / don’t collide versions.” high
Nit L8 docs/harness-limits.md Submit queue → When it runs says “protocol v20 promote gate.” The gate is v19 (inv_set_queue_promote_allowed); v20 is insert-at-front. Same class of split-brain as the README, bounded to one cell. Defender: “v20 is the current protocol.” The gate did not move. high
Nit L1 Front-insert latch is still a boolean, not a count (has_pending_front_insert / reconcileFrontInsert bumps +1 once). Two successful insertQueuedFront calls before the next Wasm frame would shift twice and bump once — saveEdit lands on the neighbor. Production give-up inserts Continue once. No second caller on this SHA. Defender: “one Continue per give-up.” Correct for this PR. high
Nit L8 AGENTS.md protocol row + docs/agent-stream.md Error row claim permanent “401/403/validation/4xx”. PERMANENT_TURN_STATUS is a whitelist (400,401,403,404,413,422); 408/429 retry 5× (and the new 408 test asserts that). A 408 Gateway timeout is retried five times while the living docs say “4xx → single attempt.” Defender: “the code comment lists 408/429 as retryable.” Then don’t write “4xx” in AGENTS / agent-stream. high

Residual risk

streamPainted is armed on tool_start / tool_result / reasoning_delta / text_delta / skill_attached only. A done event can paint via finalizeAssistant without flipping the flag; sendAgentStream still awaits onEvent before returning, and a done with text normally returns ok: true (no retry). A done-then-error SSE pair with no prior deltas would duplicate the assistant bubble — not a documented server sequence. JSON (streamAgent: false) still retries a tool-running turn; production default streams; that re-run is the existing mid-tool-cancel product rule. Later successful Ready still auto-promotes a parked Continue (goal 5, locked). usage SSE still does not arm streamPainted. Live operator smoke of a streamed 5xx-after-tools was not run in this attack.

Merge guidance

PASS WITH NOTES. Safe to merge from this attack; fix the README protocol row (v20 + both exports) in this PR or a fast follow. The leftover Nits are docs/latch, not drain/security holes. Do not --approve from this skill.

What was not attacked

Live DO runner beyond green zig → harness.wasm on this SHA (32554889823); prod Gateway tool replay; in-browser Stop-during-backoff; thinking-collapse on busy→err (existing unit already covers that transition); full vitest run was not re-executed in this review workspace (traced tests + claimed 1963/1963 in the PR body). GitHub check-runs on the head are build-harness + ignored Vercel only.

@btipling

btipling commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Plan to address feedback on PR #774 (Status: DONE)

Last updated: 2026-08-22

Feedback items (most complex → least)

# Severity Review item Status
1 Minor L8 native/harness/README.md protocol row + insert-front export still v19; promote-gate export absent 🟢 done
2 Nit L8 docs/harness-limits.md "When it runs" calls the promote gate v20 (it is v19) 🟢 done
3 Nit L1 front-insert latch is a boolean, not a count deferred
4 Nit L8 AGENTS.md + docs/agent-stream.md claim permanent "4xx"; real whitelist is 400/401/403/404/413/422 🟢 done
5 Nit L8 living docs say a retryable 5xx/timeout retries 5×; streamPainted makes any post-paint failure a single attempt 🟢 done (round 4)

#1native/harness/README.md still documents protocol 19 (🟢 DONE)

  • Grounding: lib/harnessBridge.ts:41 HARNESS_PROTOCOL_VERSION = 20, bridge.zig:44 PROTOCOL_VERSION = 20, build.zig:91,92 lists both inv_set_queue_promote_allowed and inv_queued_insert_front. README was a version behind after the protocol-union rebase.
  • Fix (committed 05bfbe3): line 114 inv_queued_insert_frontv20; added a new export-table row inv_set_queue_promote_allowed (v19, plan plan: harness — Stop/Esc never drains the queue; idle ▶ promotes when composer empty (#757) #760); line 126 Protocol version20 with the full per-version history (v18 queued_count, v19 promote gate, v20 insert-front).
  • Tests: docs-only; no TS/Zig gate per the layer-scoped table.

#2docs/harness-limits.md promote-gate cell says v20 (🟢 DONE)

  • Fix (05bfbe3): line 107 "protocol v20 promote gate armed true" → "protocol v19 promote gate inv_set_queue_promote_allowed armed true".

#3 — front-insert latch is a boolean (deferred)

  • Grounded: bridge.zig:165 has_pending_front_insert: bool; insertQueuedFront sets it once; reconcileFrontInsert bumps +1 once per consumed signal.
  • Decision: DEFER. Reviewer itself: "Production give-up inserts Continue once. No second caller today. Correct for this PR." Round-3/round-4 merge guidance: "The leftover Nit is a latch, not a drain/security hole. Safe to merge from this attack." No second host unshift source exists on this SHA. A counter (or bump-by-queuedCount delta) is documented future-proofing if a second unshift source appears — not built now.

#4 — living docs claim permanent "4xx" (🟢 DONE)

  • Grounded: lib/harnessChat.ts:121 PERMANENT_TURN_STATUS = new Set([400,401,403,404,413,422]); 408/429/5xx and timeout/empty retry 5× (new 408 test asserts that).
  • Fix (05bfbe3): AGENTS.md:298 → whitelist list + "408/429/5xx and timeout/empty retry"; docs/agent-stream.md:108 → same correction; docs/harness-limits.md:110 Permanent-failures row → whitelist + retryable note.

#5 — living docs claim "5 attempts" without the post-paint single-attempt carve-out (🟢 done, round 4)

  • Grounded: lib/harnessChat.ts:1032 streamPainted (monotonic, set on tool_start/tool_result/reasoning_delta/text_delta/skill_attached); lib/harnessChat.ts:1462-1463 classify: (err) => streamPainted ? { kind: 'permanent' } : classifyTurnRetry(err) — so a failure AFTER any ring row has painted past the user line is a single attempt, while a never-painted failure (immediate 5xx / network drop) still retries up to 5 (TURN_RETRY_ATTEMPTS). docs/harness-limits.md:108/110, docs/agent-stream.md:43/108, and AGENTS.md:298 all claimed an unconditional 5× retry — misleading for the production SSE default.
  • Fix (committed 67c001f): added the 1-attempt-once-painted carve-out to all five rows: harness-limits Turn-error recovery + Permanent failures (408/429/5xx cell), agent-stream error event row + Error/timeout/empty end-of-turn row, and AGENTS.md v20 cell.
  • Tests: docs-only (AGENTS.md, docs/harness-limits.md, docs/agent-stream.md); no TS/Zig gate per the layer-scoped table.

Progress log (append-only)

Merge decision

Round-1 Majors fixed in earlier rounds; round-3 Minor + 2 Nits fixed (05bfbe3); round-4 Nit #5 fixed (67c001f), #3 explicitly deferred (reviewer concurrence). Re-run adversarial-review on 67c001f; if clean → hand to merge-pr on explicit request.

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #774 (round 4)

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/turn-retry-never-drain-queue · 19 files · protocol v20 inv_queued_insert_front + turn retry (plan #759)
HEAD: 05bfbe32e47ed598c1556989850b0f52a77999ba
Lenses run: L1, L2, L3, L4, L6, L8 (skip: L5 no new poll/history growth; L7 no clone bind; L9 no palette)
AGENTS.md read: yes · docs/feature-divide.md not changed (host still owns /api/agent + lifecycle; Wasm still owns the FIFO) — divide holds

Round 3 (PASS WITH NOTES on a797a1f) asked for README v20 + both exports, the promote-gate cell to say v19, and the 4xx → whitelist docs. 05bfbe3 is docs-only and lands those three. Re-traced the production seams on this SHA, not the prior comments.

The shape still holds: withTransientRetry + additive classify (default still classifyVercelError; sandbox callers untouched), streamPainted fail-closes retries once SSE has painted past the user line, give-up lands Lifecycle.Error so shouldAutoPromote (cur == .ready only) cannot drain, Stop stays Ready / setQueuePromoteAllowed(false) / no Continue, insertFront fails closed at 16 / blank, both REQUIRED exports are in build.zig export_symbol_names and REQUIRED_FNS. Protocol is 20 on TS + Zig + README + wasm-int. Feature-divide one-liner: a multi-turn chat still completes from the Wasm composer; the host only arms lifecycle + unshifts Continue.

Round-3 Minor + two docs Nits are gone. No Blocker or Major survived self-refutation on this SHA.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Nit L1 Front-insert latch is still a boolean, not a count (has_pending_front_insert / reconcileFrontInsert bumps +1 once). Carried from round 3; author deferred. Two successful insertQueuedFront calls before the next Wasm frame would shift twice and bump once — saveEdit lands on the neighbor. Production give-up inserts Continue once. No second caller on this SHA. Defender: “one Continue per give-up.” Correct for this PR. Stays a latch, not a drain. high
Nit L8 Living docs this SHA just rewrote (docs/harness-limits.md Turn-error recovery + Permanent failures; docs/agent-stream.md Error row; AGENTS.md v20 cell) still say a retryable 5xx/timeout retries 5×. streamPainted makes any post-paint failure a single attempt (harnessChat.ts classify gate; covered by the live-stream test). 502 after the first reasoning_delta / text_delta / tool_start / skill_attached → 1 send, give-up + Continue. Operator reading the table expects four more tries. Queue is not drained. Defender: “fail-closed after paint is the more important lock; Continue still appears.” Then say so in the rows that claim “5 attempts.” high

Residual risk

streamPainted is armed on tool_start / tool_result / reasoning_delta / text_delta / skill_attached only. A done event can paint via finalizeAssistant without flipping the flag; sendAgentStream still awaits onEvent before returning, and a done with text normally returns ok: true (no retry). JSON (streamAgent: false) still retries a tool-running turn; production default streams. usage SSE still does not arm streamPainted. A session that emits skill_attached at turn start will skip retry of a later 5xx (duplicate skill row would be worse). insertFront still stack-allocates a 256 KiB scratch; wasm-int insert of a short Continue succeeded, so the default Wasm stack held it. Live operator smoke of Stop-during-backoff was not run here.

Merge guidance

PASS WITH NOTES. Safe to merge from this attack. Round-3 docs holes are closed. The leftover Nits are latch + a retry-count caveat, not drain/security holes. Do not --approve from this skill.

What was not attacked

Live DO runner beyond green zig → harness.wasm on this SHA (32558406080); prod Gateway tool replay; in-browser Stop-during-backoff; thinking-collapse on busy→err (existing unit already covers that transition); full vitest run was not re-executed in this review workspace (traced tests + claimed 1963/1963 in the PR body). GitHub check-runs on the head are build-harness + ignored Vercel only.

Round-4 L8 Nit: living docs claimed a retryable 5xx/timeout retries 5x, but streamPainted makes any post-paint failure a single attempt (harnessChat.ts classify gate). Adds the 1-attempt carve-out to harness-limits Turn-error recovery + Permanent failures, agent-stream error row + Error/timeout/empty row, and AGENTS.md v20 cell.

@btipling btipling left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — PR #774 (round 5)

Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: mainplan/turn-retry-never-drain-queue · 19 files · protocol v20 inv_queued_insert_front + turn retry (plan #759)
HEAD: 67c001f82038fff6eb5ff4db83b1a3b01f58fa81
Lenses run: L1, L2, L3, L4, L6, L8 (skip: L5 no new poll/history growth; L7 no clone bind; L9 no palette)
AGENTS.md read: yes · docs/feature-divide.md not changed (host still owns /api/agent + lifecycle; Wasm still owns the FIFO) — divide holds

Round 4 (PASS WITH NOTES on 05bfbe3) left two Nits: the boolean front-insert latch, and living docs claiming retryable 5xx retries 5× while streamPainted makes post-paint failures a single attempt. 67c001f is docs-only and lands the second: docs/harness-limits.md Turn-error recovery + Permanent failures, docs/agent-stream.md SSE error + Error/timeout/empty, and the AGENTS.md v20 cell all state 1 attempt once a ring row has been painted. Re-traced the production seams on this SHA.

The shape still holds: withTransientRetry + additive classify (default still classifyVercelError), streamPainted fail-closes retries once SSE has painted past the user line, give-up lands Lifecycle.Error so shouldAutoPromote (cur == .ready only) cannot drain, Stop stays Ready / setQueuePromoteAllowed(false) / no Continue, insertFront fails closed at 16 / blank, both REQUIRED exports are in build.zig export_symbol_names and REQUIRED_FNS. Protocol is 20 on TS + Zig + README + wasm-int. Feature-divide one-liner: a multi-turn chat still completes from the Wasm composer; the host only arms lifecycle + unshifts Continue.

Round-4 docs Nit is gone. No Blocker or Major survived self-refutation on this SHA.

Findings

Sev Lens Finding Break scenario Refutation attempt Confidence
Nit L1 Front-insert latch is still a boolean, not a count (has_pending_front_insert / reconcileFrontInsert bumps +1 once). Carried from round 3; author deferred. Two successful insertQueuedFront calls before the next Wasm frame would shift twice and bump once — saveEdit lands on the neighbor. Production give-up inserts Continue once. No second caller on this SHA. Defender: “one Continue per give-up.” Correct for this PR. Stays a latch, not a drain. high

Residual risk

streamPainted is armed on tool_start / tool_result / reasoning_delta / text_delta / skill_attached only — now disclosed in living docs. Production SSE almost always paints (skill row at start, then thinking/tools), so the 5-attempt loop is the clean (never-painted) path; post-paint 5xx is single-attempt give-up + Continue. That is fail-closed against duplicate tool/bubble replay (#756 Goal 2’s “same prompt 5×” is the unpainted case). Follow-up for “continue the failed work from the composer without consuming the queue” is #776 (pause), not this PR. JSON (streamAgent: false) still retries a tool-running turn; production default streams. usage SSE still does not arm streamPainted. insertFront still stack-allocates a 256 KiB scratch; wasm-int insert of a short Continue succeeded. Live operator smoke of Stop-during-backoff was not run here.

Merge guidance

PASS WITH NOTES. Safe to merge from this attack. Round-4 docs hole is closed. The leftover Nit is the deferred boolean latch, not a drain/security hole. Do not --approve from this skill.

What was not attacked

Live DO runner — zig → harness.wasm is pending on this SHA (32559474648; native unchanged vs the previously green head). Prod Gateway tool replay; in-browser Stop-during-backoff; thinking-collapse on busy→err; full vitest run was not re-executed in this review workspace (traced tests + claimed 1963/1963 in the PR body). GitHub check-runs otherwise ignored Vercel only.

@btipling
btipling merged commit 069952c into main Aug 22, 2026
3 checks passed
@btipling
btipling deleted the plan/turn-retry-never-drain-queue branch August 22, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plan: harness — turn errors retry the current turn, never drain the queue (#756) · IMPLEMENTED (PR #774)

1 participant