Retract recovered inference errors on failover - #711
Conversation
Committed retry already rolled back the failed attempt. Failover is a new inference.start, so the error row stayed. Treat that start as recovery too, so quota and credential chrome disappear when the turn continues.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Lens: critic. Verdict: no blocking findings.
This branch retracts recovered inference-error rows on same-turn failover, preserves local queue/steer rows during rollback, and keeps terminal failures visible. Re-review verified the prior echo and local-row rollback holes are fixed. I found no remaining blocking or should-fix correctness issues.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Critic · Comment
Failover inference.error then inference.start retracts recovered quota/credential rows; queued/steer rows survive truncate.
Findings
src/tui/runtime-bridge.ts:429— global echo skip keepserrorRollbackArmedacross a new turn. Sequence: error (handoff armed) → interrupt (idle, handoff still armed) → immediate submit → echoedmessage.received(mapping skipped) →inference.start(ATTEMPT_ROLLBACK). Untagged user rows, the stopped line, and the terminal error disappear. AC 4 and AC 6 collide.src/tui/stream-event-map.ts:168—connector.replyis not retry/start, so the one-event handoff dies before TUI long-window auto-retry can retract.src/tui/runtime-bridge.test.ts:975— terminal test only assertserrorRows.length > 0. A swallowed session-expired line plusfailedstill passes. Pin the classified copy.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Neckbeard · Comment
Rollback copies operator rows out, truncates, then re-appends them.
Findings
src/tui/runtime-bridge.ts:726—Array.from({ length })plus a type-predicate filter walks[boundary, count)instead of a for-loop.src/tui/runtime-bridge.ts:712—isLocallyQueuedUserRowtreats drain tags (steering,following-up) as queued and omitsreinject.src/tui/runtime-bridge.ts:707— rollback JSDoc sits onisLocallyQueuedUserRow, which does not retract anything.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Request changes
Same-turn failover retracts recovered inference-error rows and re-appends queued/steer user rows.
GitHub refuses request-changes on the author's own PR, so this is a comment with the same blocking bar.
Findings
src/tui/runtime-bridge.ts:1080—consumePendingEchoEventreturns fromhandlebeforemapProductionEvent, so amessage.receivedecho never expireserrorRollbackArmed. Interrupt does not clear that handoff orattemptRow. A later immediate prompt is untagged (runtime-bridge.ts:1163), then the nextinference.startrolls back from the old mark and deletes the new prompt, the stopped row, and the terminal error. Scope the echo skip to in-flight recovery, or expire the handoff on interrupt.src/tui/runtime-bridge.ts:712— rollback only re-appendsqueue/steer/steering/following-up. Immediate submits have no meta, so they disappear on that rollback.src/tui/runtime-bridge.test.ts:900— echoed auto-retry test never asserts the echoed prompt survives, and does not cover interrupt then a new prompt.src/tui/stream-event-map.ts:296—connector.replyexpires the one-event handoff, so a director-visible quota error followed by the TUI quota-loop replay cannot retract the row.
Reinject still stops the run before sending. Clear the one-event handoff there too, and keep reinject rows if a rollback still fires.
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Request changes
Treats a same-turn failover inference.start after inference.error as recovery and rolls back the failed attempt including its error row, keeps locally queued user rows across that rollback, short-circuits echoed message.received events before the mapper, and expires the handoff on interrupt/reinject.
Findings
src/tui/runtime-bridge.ts:1081—if (consumePendingEchoEvent(bag, event)) return;skips the bookkeeping every echoed user message used to run viaapplyInbound:bag.turnThinking = nullat:848, pluscloseOpenRow/disarmAttemptinstream-event-map.ts:328. After a steer or follow-up delivered at a tool boundary, the next cycle's thinking deltas fold into the pre-steer reasoning row above the operator's message instead of opening a new row below it. Reproduced against both trees with: submit → echo →inference.start→ thinking "planning" →tool_call.end→inference.done→submit(..., "steer")→tool.done→ echo →inference.start→ thinking "after steer" → text. main:thinking:"planning", tool, user/steer,thinking:"after steer", assistant. branch:thinking:"planning\n\nafter steer", tool, user/steer, assistant. RunningcloseOpenRow(shell, bag); bag.turnThinking = null;in the short-circuit path restores the split. None of the new tests assert thinking-row placement after a steer echo.src/tui/runtime-bridge.test.ts:906-945— the test never asserts the fate of the "retry this" user row. It is a plainrole:"user"row (no meta,runtime-bridge.ts:1166) sorollbackAttempttruncates it (isLocallyQueuedUserRowat:707only spares meta rows). Probably desired for the quota auto-replay, but it is unstated — pin it either way.
Notes
isLocallyQueuedUserRowcovering"queue"and"steer"metas is correct; those rows sit after the boundary only by ordering.- Checks:
bun run typecheck,bun run lint,bun test src/tui/stream-event-map.test.ts src/tui/runtime-bridge.test.ts(77 pass).
Consuming an echoed message before the mapper kept the recovery handoff alive but skipped the turn bookkeeping, so reasoning after a steer folded into the row above the operator's message. Close the open row and reset the turn thinking on that path, and pin that the replayed quota prompt row is dropped by the rollback.
2e6f6ba to
aa031e9
Compare
TheGreatAxios
left a comment
There was a problem hiding this comment.
Critic · Approve
Same-turn failover after inference.error retracts the recovered error row; echoed auto-retry no longer expires the handoff; interrupt/reinject expire it; locally queued/steer rows survive rollback. Prior review holes are closed in the current tip.
Findings
Blocking: none.
Merge readiness: ready. CI green (build-and-test, typecheck, eslint, prettier, cla). Targeted tests pass (stream-event-map, runtime-bridge).
Verified against tip (aa031e9c):
stream-event-map.ts:168/:333—recoversErrorHandofftreatsinference.startlikeinference.retry; recovered start emitsATTEMPT_ROLLBACKthen remakes the boundary.runtime-bridge.ts:1081— echoedmessage.receivedskips the mapper so the one-event handoff survives, and still runscloseOpenRow+ clearsturnThinkingso post-steer reasoning opens a new row.runtime-bridge.ts:1207/:1148— interrupt and reinject clearerrorRollbackArmedandattemptRow, so a later new-turn start cannot erase a terminal error or the operator's next prompt.runtime-bridge.ts:707/:723— rollback re-appendsqueue/steer/steering/following-up/reinjectrows after truncate.
File-for-later (non-blocking):
runtime-bridge.ts:1081— echo short-circuit skips mapperdisarmAttempt. Product mid-run Enter is meta-tagged (steer/queue) and survives rollback; a directsubmit(..., "immediate")while an attempt is still armed could still lose an untagged user row on a later retry. Not reachable from current product chords.- Long-window TUI quota auto-retry is intentionally durable (
turn-monitor.test.tscomment); the one-event handoff is for same-turn recovery only. Out of scope here.
Recommended permanent tests (already present; keep)
- Failover start after quota/credential error clears the error row.
- Echoed auto-retry keeps recovery and drops the duplicate prompt.
- Interrupt/reinject then new prompt keeps classified error + prompt.
- Queued steer survives failover rollback.
- Steer echo opens a new thinking row below the steering line.
Summary
Verification
Fixes CL-7119