You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Silent SSE terminations: EOF without a finish frame completes the turn, chunkTimeout misses stalled streams, provider error bodies are discarded #39968
Version: opencode 1.18.2 (also present on current dev), Linux, openai-compatible provider behind an HTTP gateway.
Over a ~12h window of a long-running multi-agent workload (13,312 model requests), the gateway terminated 23 in-flight SSE completion responses with a bare EOF: connection close with no finish frame, no [DONE], no error event (~0.17% of requests; several breaks landed within ~1s of each other on independent machines, so a shared upstream cause). A 24th connection stalled half-open instead of closing. opencode's handling turned this transport blip into wrong results three ways:
EOF without a finish frame concludes the turn as a success. When the stream ends without a finish frame, the AI SDK emits its synthesized fallback finish-step (finishReason stays at the initial "other", surfaced as unknown, all-zero usage). opencode persists it as a completed turn and opencode run exits 0 with the truncated output. In our window: 7 truncated turns accepted as complete, and 16 mid-tool-call truncations whose cut-off tool JSON was absorbed by experimental_repairToolCall as an invalid tool call — the agents then misattributed the failure ("payload too large") and permanently changed strategy. There is even a test pinning the exit-0 behavior (unknown stream finish preserves partial output and exits 0).
chunkTimeout does not bound a stalled model stream. The option is enforced only in the fetch-level SSE wrapper (wrapSSE in packages/opencode/src/provider/provider.ts), which watches raw bytes. Any bytes — SSE keepalive/comment lines included — reset that timer even when the parsed model stream delivers nothing, and a stall in any layer above the byte stream never trips it. The model-call path (streamText consumption in packages/opencode/src/session/llm.ts / session/processor.ts) has no per-chunk deadline at all. One stage sat on a half-open connection for 9.5 hours with chunkTimeout: 300000 configured.
Provider rejections lose their response bodies.ProviderError.message() (packages/opencode/src/provider/error.ts) returns the SDK's message alone whenever it differs from the bare HTTP status text and discards responseBody. A 400-class rejection surfaced as a bare Invalid request. with nothing to diagnose.
Suggested behavior:
treat EOF-without-finish (fallback finish-step: reason other/unknown + no raw finish reason + no usage) as a retryable stream error with a small bounded in-turn retry budget, failing the turn (nonzero exit) on exhaustion instead of exiting 0 with half a turn;
enforce the existing chunkTimeout option on the parsed-chunk gap in the streamText consumption path (disarmed while local tool calls execute), aborting the request on expiry;
keep a bounded copy of the provider response body in the surfaced error message.
I have a fix for all three with regression tests and will open a PR referencing this issue.
Activity
github-actions commentedon Jul 31, 2026
This issue might be a duplicate of existing issues. Please check:
chunkTimeoutgap (no default, byte-level timer does not bound parsed-chunk stalls)If these capture your scenarios, it may be worth coordinating a fix or referencing them in your upcoming PR.
github-actions commentedon Sep 30, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.