Recurrent on my machine. A turn dies mid-stream and is deliberately not retried, so the user just loses it:
WebSocket closed before response.completed (code 1006: socket closed)
(continuation of resp_0507483a3bdfa140016aa4748e597c87d09bc900a67570f5f0)
(not retried: output already emitted)
This is distinct from #135, which was the 1009 Message Too Big case and is fixed. Here the peer closes the TCP connection with no close frame at all.
What v0.7.1's new fields say about it
The dual-id logging and frame counters added in v0.7.1 make this much more legible than it was in September. The full record for the instance above:
[2026-09-11T21:37:39.363Z] WARN [transport] stream failed after output; not retried
reason: "WebSocket closed before response.completed (code 1006: socket closed)"
emittedOutput: true
responseID: resp_0507483a3bdfa140016aa474963b6487d0b4aa987513e167c2
previousResponseID: resp_0507483a3bdfa140016aa4748e597c87d09bc900a67570f5f0
hasContinuation: true
framesSinceCreated: 4
msSinceLastFrame: 9099
Four frames after response.created, then nine seconds of silence, then the socket dies. It is a continuation carrying previous_response_id. That matches the profile I measured across seven instances on #135: follow-on continuations that die in the opening seconds, silence windows of 4-11s, no close frame.
The part worth changing
emittedOutput is set for every event except response.created and response.in_progress (ws.ts, the translatedEvent.type !== 'response.created' && ... !== 'response.in_progress' branch). With four frames total, what flipped it was something like output_item.added or a reasoning-summary part - no user-visible text, no completed tool call.
So the no-replay gate from #92 is doing the right thing for the wrong reason here. Its purpose is to prevent duplicated text and re-run side-effecting tools, and neither is at risk after four lifecycle-adjacent frames. But emittedOutput cannot tell that apart from a response that streamed two paragraphs and called a tool.
A sharper predicate, using what v0.7.1 already measures: replay is unsafe once a response.output_item.done has been seen (opencode dispatches tool execution on that event) or once any text delta has reached the user. Before either, a 1006 with no close frame is indistinguishable from a connection that never produced anything, and the existing HTTP fallback path added for 1009 in e68ab8f already knows how to serve a request whose socket died before output.
I am not suggesting the gate be relaxed generally - it exists because retrying after real output duplicates text and double-bills. The suggestion is that emittedOutput is coarser than the invariant it protects, and the frame counters now give enough information to narrow it.
Frequency
One occurrence in the current log window (which starts at 20:41 today). I see it regularly across sessions; #135 carries seven measured instances from 09-05 and 09-06 with the same signature. Happy to collect a larger sample with /openai-dump on if the frame profile above is not enough to work from.
Version: v0.7.1 (fb1402e), Bun raw WebSocket client, Linux.
Recurrent on my machine. A turn dies mid-stream and is deliberately not retried, so the user just loses it:
This is distinct from #135, which was the 1009 Message Too Big case and is fixed. Here the peer closes the TCP connection with no close frame at all.
What v0.7.1's new fields say about it
The dual-id logging and frame counters added in v0.7.1 make this much more legible than it was in September. The full record for the instance above:
Four frames after
response.created, then nine seconds of silence, then the socket dies. It is a continuation carryingprevious_response_id. That matches the profile I measured across seven instances on #135: follow-on continuations that die in the opening seconds, silence windows of 4-11s, no close frame.The part worth changing
emittedOutputis set for every event exceptresponse.createdandresponse.in_progress(ws.ts, thetranslatedEvent.type !== 'response.created' && ... !== 'response.in_progress'branch). With four frames total, what flipped it was something likeoutput_item.addedor a reasoning-summary part - no user-visible text, no completed tool call.So the no-replay gate from #92 is doing the right thing for the wrong reason here. Its purpose is to prevent duplicated text and re-run side-effecting tools, and neither is at risk after four lifecycle-adjacent frames. But
emittedOutputcannot tell that apart from a response that streamed two paragraphs and called a tool.A sharper predicate, using what v0.7.1 already measures: replay is unsafe once a
response.output_item.donehas been seen (opencode dispatches tool execution on that event) or once any text delta has reached the user. Before either, a 1006 with no close frame is indistinguishable from a connection that never produced anything, and the existing HTTP fallback path added for 1009 ine68ab8falready knows how to serve a request whose socket died before output.I am not suggesting the gate be relaxed generally - it exists because retrying after real output duplicates text and double-bills. The suggestion is that
emittedOutputis coarser than the invariant it protects, and the frame counters now give enough information to narrow it.Frequency
One occurrence in the current log window (which starts at 20:41 today). I see it regularly across sessions; #135 carries seven measured instances from 09-05 and 09-06 with the same signature. Happy to collect a larger sample with
/openai-dump onif the frame profile above is not enough to work from.Version: v0.7.1 (
fb1402e), Bun raw WebSocket client, Linux.