Follow-up to #92, filed at @ualtinok's request. Not a defect — a known, deliberately-accepted cost with a possible better answer.
Where things stand after #92
The no-replay gate keys on emittedOutput: once a frame carrying generated output has reached the consumer, a transport failure ends the turn instead of rerouting.
That replaced a coarser gate on emitted (any enqueued frame, including response.created), which suppressed reroutes for the entire window before the first token.
An intermediate proposal during review was to gate on user-visible text. That was rejected, and the reasoning is worth restating because it constrains any future fix: response.output_item.done carrying a function_call produces no text at all, yet is exactly the point after which replay is unsafe. Text-gating does not trade safety for reroutes — it misses the expensive case outright.
The cost we are currently paying
A transport failure in the window after the first output item but before anything the user would notice ends the turn rather than rerouting. We give up a reroute to avoid a double charge. Conservative and correct, but not free.
The proposal
The discriminator that would recover that window is dispatch, not visibility: a frame OpenCode has not acted on yet is safe to replay.
If OpenCode's parser buffers a function_call until arguments are complete, then a failure mid-arguments may be replayable — nothing was dispatched, nothing was billed twice, no side effect ran. The current gate closes at the first output item and cannot distinguish that.
What this needs before anyone implements it
- Determine how OpenCode's parser dispatches tool calls — specifically whether there is an observable point before which a
function_call frame has provably not been acted on.
- Determine whether that point is stable enough to depend on, or whether it is an implementation detail that would silently regress the guarantee when upstream changes.
- Confirm the consumer has not already rendered or committed continuation state at that point.
If (1) or (2) does not hold, the honest outcome is to close this and keep the current gate — a slightly-too-conservative gate that is obviously correct beats a precise one that depends on upstream internals staying put.
Constraint for whoever picks this up
The asymmetry is not symmetric and should drive every judgement call: over-gating costs a reroute on a recoverable failure — visible, recoverable, annoying. Under-gating duplicates output, re-runs side-effecting tools, and double-bills — silent and unrecoverable.
ws.ts currently carries a comment pointing here, so this issue is the place a future reader lands instead of re-deriving text-gating.
Follow-up to #92, filed at @ualtinok's request. Not a defect — a known, deliberately-accepted cost with a possible better answer.
Where things stand after #92
The no-replay gate keys on
emittedOutput: once a frame carrying generated output has reached the consumer, a transport failure ends the turn instead of rerouting.That replaced a coarser gate on
emitted(any enqueued frame, includingresponse.created), which suppressed reroutes for the entire window before the first token.An intermediate proposal during review was to gate on user-visible text. That was rejected, and the reasoning is worth restating because it constrains any future fix:
response.output_item.donecarrying afunction_callproduces no text at all, yet is exactly the point after which replay is unsafe. Text-gating does not trade safety for reroutes — it misses the expensive case outright.The cost we are currently paying
A transport failure in the window after the first output item but before anything the user would notice ends the turn rather than rerouting. We give up a reroute to avoid a double charge. Conservative and correct, but not free.
The proposal
The discriminator that would recover that window is dispatch, not visibility: a frame OpenCode has not acted on yet is safe to replay.
If OpenCode's parser buffers a
function_calluntil arguments are complete, then a failure mid-arguments may be replayable — nothing was dispatched, nothing was billed twice, no side effect ran. The current gate closes at the first output item and cannot distinguish that.What this needs before anyone implements it
function_callframe has provably not been acted on.If (1) or (2) does not hold, the honest outcome is to close this and keep the current gate — a slightly-too-conservative gate that is obviously correct beats a precise one that depends on upstream internals staying put.
Constraint for whoever picks this up
The asymmetry is not symmetric and should drive every judgement call: over-gating costs a reroute on a recoverable failure — visible, recoverable, annoying. Under-gating duplicates output, re-runs side-effecting tools, and double-bills — silent and unrecoverable.
ws.tscurrently carries a comment pointing here, so this issue is the place a future reader lands instead of re-deriving text-gating.