Skip to content

fix(core): the LLM stream idle ceiling — a hung read fails through the transport path, the turn unwinds, busy frees - #1759

Closed
aarontrowbridge wants to merge 1 commit into
jev-routed-poolfrom
opencode/issue-1739-stream-idle-timeout
Closed

aarontrowbridge wants to merge 1 commit into
jev-routed-poolfrom
opencode/issue-1739-stream-idle-timeout

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

Closes #1739. Part of #1731 (panel-reliability lane — the idle-timeout doctrine's third leg: client #1752, upstream #1758, now the engine). Stacks on #1721 (cut from jev-routed-pool @f20ee140).

The fix — the lifecycle family's last member

withStreamIdleCeiling wraps the ONE llm.stream(request) (preserved — the wrapper consumes the same single stream object): Stream.fromPull(Stream.toPull(stream)) with each pull raced by Effect.timeoutOption(idleMs) — reset-on-read, never a busy-wait. On a hung read the stream fails with LLMError(TransportReason{kind:"idle-timeout"}) — no new error vocabulary: it enters the exact branch that already handled transport failures (failAssistant + failCause), so the durable machinery unwinds identically — the turn errors, the run-coordinator registration frees, the busy state clears, and a re-prompt resumes to a next provider turn (all pinned).

Config: AMICODE_STREAM_IDLE_TIMEOUT_MS, default 90,000 (inside the issue's 60–120s band — reasoning deltas arrive continuously, so 90s of dead air is dead transport), read per-turn at call time, invalid/non-positive falls back to default. Mirrors the AMICODE_MAX_STREAM_* knob family (#775/#1716).

The repro it kills: today's live case #3 — a session went mid-turn silent (reasoning/text/tool in flight, zero event frames for 34s, engine otherwise healthy), held busy until POST abort cleared it; the issue's original case held busy 8 hours. With this ceiling, the same hang fails the step within 90s and the session recovers on re-prompt.

Gates (orchestrator-run, baselines captured pre-edit)

  • Core unit suite: 1107 pass / 2 fail = baseline 1105/2 + 2 new green, zero new failures (the 2 are the documented pre-existing lane pair, stable across 4 runs); targeted runner file 85/2 same pair
  • Engine unit gate + engine/overlay typecheck gates: fail lists byte-identical to pre-edit baselines (the known lane pre-existing sets — Session-lineage spec conflict: legacy-parent aggregation vs 'Bug A fix' (auto-create) — tied to server-survives-rebuild #1239 family, stacked-branch TS2345 pair, server-sdk.tsx TS2339) — zero delta from this slice
  • materialize.mjs manifest verified at exact hashes (hand-patched sha256s, no auto-refresh noise); drift_gate.mjs PASS
  • Tests pin: the held-then-freed execution.active registration; assistant finish:"error" with flushed partial content; re-prompt resumes to a second provider turn; 60ms-gap stream under a 300ms ceiling never trips (reset-on-read); the RED pre-fix run confirmed the leak (resume parked forever, bounded only by the test's own 5s timeout)

Honest notes

  • The 90s default is not directly wall-clock-asserted (not worth its runtime); the env-override path + invalid-fallback derivation are what the tests pin. Seam gap recorded.
  • The issue's operational bridge (a frontdoor stuck-busy warning card) is out of scope here — engine fix only.
  • Deploy: engine fix → needs a binary rebuild + engine swap before it reaches running instances (the orchestrator's ops step; cycling shards bounces in-flight turns fleet-wide).
  • Two engine-API realities recorded for the ledger: effect@4-beta Pull is an Effect object, not callable (Effect.suspend is the per-run wrapper); it.effect runs under TestClock so wall-clock timing tests must use it.live.

…er an idle ceiling

A provider socket that dies without a clean FIN/EOF parked the stream read
forever, leaking the turn, its run-coordinator registration, and the session's
busy state for hours (#1739). Race every stream read against a configurable
idle ceiling (AMICODE_STREAM_IDLE_TIMEOUT_MS, default 90s) — reset-on-read, so
healthy slow generations never trip it — and fail the step with an
LLMError(TransportReason) through the existing turn-failure path, which unwinds
the turn, publishes the failed assistant message, frees the busy registration,
and lets a re-prompt resume. The one explicit llm.stream(request) call per
provider turn is untouched; the ceiling wraps the read loop only.
@coderabbitai

coderabbitai Bot commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8320c8b7-74a0-4a6e-826e-6bb49d22be7d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@aarontrowbridge

Copy link
Copy Markdown
Member Author

Superseded: the consolidation #1833 merged live-test/stack-e2e into main (2026-10-21, Aaron's window) — this branch's content is contained in main (ancestor-verified during the drain-plan analysis). Closing with this crosspointer; the work is landed.

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.

1 participant