Skip to content

WS replay gate: discriminate on dispatch rather than on generated output #105

Description

@iceteaSA

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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions