Skip to content

fix(streaming): detect truncated streams and complete promptly on [DONE] - #170

Open
Fahad090NP wants to merge 1 commit into
ltmoerdani:mainfrom
Fahad090NP:fix/stream-silent-stop
Open

fix(streaming): detect truncated streams and complete promptly on [DONE]#170
Fahad090NP wants to merge 1 commit into
ltmoerdani:mainfrom
Fahad090NP:fix/stream-silent-stop

Conversation

@Fahad090NP

Copy link
Copy Markdown
Contributor

Summary

The shared streaming engine (src/transports/engine.ts) only ended a response stream on connection close or user cancellation, ignoring OpenCode's data: [DONE] terminator (confirmed in tmp/opencode-dev/.../server/transport/ws.ts, which enqueues data: [DONE]\n\n then closes). This produced two silent failure modes that looked like "the model stopped working":

  1. Hang-then-cancel — if the gateway kept the connection alive after [DONE], the read loop never saw done and sat until the 2-minute idle timeout (user cancelled manually, no error shown).
  2. Silent truncation — if the connection dropped before [DONE] (proxy reset, upstream crash, truncated payload, VPN/firewall cut), reader.read() returned done with no [DONE] and no finish_reason; the engine treated it as a successful empty response.

Fix

  • src/transports/sse.tsparseServerSentEvent now takes an onDone callback fired on data: [DONE] (and [DONE] is no longer passed to the extractor).
  • src/transports/engine.ts — tracks streamFlags.sawDone and breaks the read loop as soon as [DONE] arrives (prompt completion instead of waiting on a lingering socket); after the loop, isStreamTruncated throws a clear OpenCodeRequestError when the stream ended with neither [DONE] nor a captured finish_reason after content was already received, instead of silently succeeding.
  • src/transports/sse.tsisStreamTruncated is a small pure helper (exported, unit-tested).
  • src/test/sse.test.ts — unit tests for [DONE] handling and isStreamTruncated.

Verification

  • npm run lint (editorconfig, eslint, markdown, prettier, shellcheck, typecheck, unit tests) — green.
  • src/test/sse.test.ts — new tests pass.

Builds on the resilience work in #169 (history-trim + transient fetch-retry).

The shared streaming engine only ended a response on connection close or cancellation, ignoring OpenCode's data: [DONE] terminator. This left the request hanging until the idle timeout when the gateway kept the connection alive after [DONE] (user cancelled silently), and treated a connection that closed without [DONE] (proxy reset, upstream crash, truncated payload) as a successful empty response.

- parseServerSentEvent now fires an onDone callback on data: [DONE] and no longer passes it to the extractor.
- engine breaks the read loop as soon as [DONE] arrives (prompt completion instead of waiting on a lingering socket).
- isStreamTruncated throws a clear OpenCodeRequestError when the stream ended with neither [DONE] nor a captured finish_reason after content was already received, instead of silently succeeding.
- unit tests added in src/test/sse.test.ts.
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