fix(harness): retry silent model stream interruptions - #2157
Conversation
There was a problem hiding this comment.
LGTM: The transient retry and lease-backed stream-silence handling are coherent and covered by the required regression scenarios.
Note
Approved · head b82be7f · no findings
Full review
No findings; the retry classification and stream-silence handling satisfy the contract without contradicting the touched specs.
There was a problem hiding this comment.
Not a finding for this PR (revised 20:02Z, after a second read with the tracker session). The lease-derived httpIdleTimeoutMs is the principled bound for pi's single knob, and the red contract test only needs its expected settings file updated to the new field. Merge on that.
Residual, carried to #1932 as the remaining item (Wave B, not this PR): pi's one knob sets both the request bound and the between-byte idle bound to the remaining lease, so an upstream that hangs silently without closing is detected only at lease end — up to the coding lease of "thinking". The right hop for a short idle bound is the proxy (src/channels/modelProxy.ts), which sees OpenRouter's SSE keepalive comments (: OPENROUTER PROCESSING) while a model reasons: it can hold a minutes-sized upstream-idle bound from the budgets module and end the client stream with the typed transient verdict, while pi's own bound stays the lease. An upstream that closes is already caught by the proxy's close → pi's transient path this PR adds. Fixture for that follow-up: b03035f7's 28 s silence, replayed as a silent hang past the bound.
Co-Authored-By: coreplane-switchboard[bot] <318072483+coreplane-switchboard[bot]@users.noreply.github.com>
b82be7f to
d6369de
Compare
There was a problem hiding this comment.
LGTM: The rebase preserves the reviewed patch, updates its contract expectation, and the two previously failing CI checks now pass.
Note
Approved · head d6369de · no findings
Full review
The rebased head resolves both prior CI findings; the bot gate and test shard now pass.
Retries OpenAI EOFs and open-stream aborts on the lease-backed ladder. Lease-sized pi timeouts and SSE keepalives prevent quiet reasoning turns from being mistaken for transport failures.
Why: #1932 showed a 29 s silent reasoning pause ending a coding child after #2136 could only salvage its work. Under record 0046, recovery must spend the existing lease instead of ending the child.
Where to look
Feedback wanted: Please focus on whether SSE comments preserve each wire dialect and whether intentional aborts remain safely ahead of transient classification.
Risk: If wrong, quiet streams can still die, provider bytes can be mishandled, or deliberate cuts can duplicate turns. The blast radius is pi-backed model calls; revert this PR to restore the prior behavior.
Verified: 347 scoped tests and all local changed-set gates passed; 29/29 CI checks completed without failure at d6369de. Deployed-hop behavior remains human-gated.
Decisions (3)
Validation (9 criteria)
npx vitest run src/core/harness/contract.test.tsfailed 1/17 on missing httpIdleTimeoutMs; after the fix, 17/17 passed.npx vitest runon 7 named affected test files — 7 files, 347 tests passed.NODE_OPTIONS=--max-old-space-size=6144 npx tsc --noEmit -p tsconfig.json— passed.npx prettier --checkon the 12 changed files — all matched.npx eslinton changed TypeScript files — passed.npm run hygiene:checkandnpm run clock:check— passed.npm run specs:checkandnpm run specs:coverage -- --changed origin/main...HEAD --test-guard --require— passed.npm run check:pr-title -- "fix(harness): retry silent model stream interruptions"— passed.ci / bot / test 1 of 4and aggregateci / botpassed; all 29 check runs completed with zero failures.For agents
Rebased without conflict onto origin/main at 4bcc7f0, folded the stale contract expectation into the single implementation commit, and force-pushed d6369de. The original shard failure was
src/core/harness/contract.test.ts: settings now correctly include the 45-minute lease-backedhttpIdleTimeoutMswith and without compaction. Reproduction wording remainsStream ended without finish_reasonandThis operation was aborted. The heartbeat is an SSE comment and deliberately bypasses TTFT, token metering, and provider-byte accounting.Requested by @justinhelmer in slack:C0BRRHKFLCB
🤖 Generated with Claude Code