fix(session): retry empty unknown-finish streams instead of stopping - #43881
moritzscheele wants to merge 1 commit into
Conversation
A provider or gateway can close the SSE stream cleanly without emitting content or a finish frame. The AI SDK then reports a step finish reason of "unknown" with zero usage, the processor persists finish=unknown, and the prompt loop treats any non-tool-calls finish as a completed turn. The session ends silently and looks like a normal completion. Fail such streams with ProviderError.ResponseStreamError after a clean drain. MessageV2.fromError already maps that error to a retryable APIError, so the existing SessionRetry policy applies backoff and retries, and halt surfaces an error if all retries fail. Observed against the opencode gateway: 189 logged stream errors in one day, with repeated assistant messages holding finish=unknown, zero output tokens, and no error field. Fixes anomalyco#41469, fixes anomalyco#43622
|
The following comment was made by an LLM, it may be inaccurate: Based on my search, I found several related PRs that address similar issues with empty/truncated provider streams and unknown finish reasons: Potentially Related PRs
These PRs appear to tackle the same root problem of empty/unknown finish streams and silent session stops, though this PR (43881) may take a different or more comprehensive approach than its predecessors. |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
|
Long-running verification finished, result: green. Setup: desktop build with this patch, sidecar running from Observations for the test window:
One honest caveat: the clean-EOF variant this PR targets did not recur during the window — the observed failures were thrown API errors that the existing retry path already handles. So the guard served as a safety net here; the before/after difference in persisted dead messages is the strongest signal I can offer from live data. |
…co#42150 anomalyco#42176 anomalyco#43881 anomalyco#43607) - O(N) text/reasoning delta accumulation instead of O(N^2) string concat (anomalyco#42150) — the lazy chunk buffer joins on read - finish reason 'error' is set when a stream fails mid-flight (anomalyco#42176) - clean-EOF empty provider streams retry like transient errors (anomalyco#43881), narrowed from the upstream patch: only an attempt that produced no text/reasoning delta and no tool call qualifies, so providers that stream content but omit usage/finish are not retried into duplicate output - SSE comment heartbeats no longer reset the chunk timeout (anomalyco#43607); the streaming TextDecoder handles multi-byte characters split across reads (regression covered)
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
Issue for this PR
Closes #41469
Closes #43622
Type of change
What does this PR do?
A provider or gateway can close the SSE stream cleanly without sending content or a finish frame. The AI SDK then reports a step finish reason of
unknownwith zero usage. The processor persistsfinish=unknown, and the prompt loop only excludestool-callsfrom its exit check, so the turn ends silently and looks like a normal completion.I hit this repeatedly on
opencode/x-preview-f-free: 189 loggedstream errorentries in one day, and many assistant messages stored withfinish=unknown, zero output tokens, and no error field. Their parts contain onlystep-start+step-finish(reason=unknown), which shows the stream ended cleanly instead of throwing.The fix fails such streams after a clean drain: if
finish === "unknown"and output tokens are zero (and compaction did not request the early stop),processfails withProviderError.ResponseStreamError.MessageV2.fromErroralready maps that to a retryableAPIError, so the existingSessionRetry.policyapplies backoff and retries, andhaltsets a visible error if all retries fail. Streams that endunknownbut produced real output are untouched.Note: #41466 addresses the same problem. This PR was developed independently and keeps the change smaller by reusing the existing error type instead of adding a new one.
How did you verify your code works?
bun run typecheckpassesbun test test/session/processor-effect.test.ts test/session/retry.test.ts: 77 passScreenshots / recordings
Not a UI change.
Checklist