Part of #118.
Across a multi-round gateway tool loop, each upstream round's raw frames are forwarded without reconciling them into one logical response. Three symptoms, one root cause (no stage owns the final client-visible sequence):
- Duplicate lifecycle —
response.created/response.in_progress forwarded every round. Live: response.created = 2 on a 2-round stream. A client that initializes on response.created double-resets.
sequence_number incoherent — upstream seq forwarded verbatim, so it resets to 0 at each new round (live: 953 → 0), and the synthetic gateway frames + the terminal chunk carry no sequence_number at all. Breaks clients that order/dedupe by seq.
output_index resets — forwarded upstream deltas keep their per-round index (restarts at 0), colliding with prior rounds. Live: index 3 → 0 at round 2; round-2 output overwrites round-1 at the same index.
Mechanism: the hide/classify/emit logic lives in three places today (public_output_items blocking, emit_gateway_*_events streaming, should_hide_upstream_event live-hide in upstream.rs). A single GatewayAccumulator stage (the #83 sketch: Raw → GatewayAccumulator → Public) that owns the final client-visible sequence fixes all three at one seam and renumbers/dedupes consistently.
Acceptance: on a ≥2-round gateway stream — exactly one response.created, monotonic sequence_number on every emitted frame (incl. synthetic + terminal), contiguous output_index across rounds.
Coordinating with @maralbahari on this — she sketched the GatewayAccumulator shape on #83 and owns accumulator.rs.
Part of #118.
Across a multi-round gateway tool loop, each upstream round's raw frames are forwarded without reconciling them into one logical response. Three symptoms, one root cause (no stage owns the final client-visible sequence):
response.created/response.in_progressforwarded every round. Live:response.created= 2 on a 2-round stream. A client that initializes onresponse.createddouble-resets.sequence_numberincoherent — upstream seq forwarded verbatim, so it resets to 0 at each new round (live: 953 → 0), and the synthetic gateway frames + the terminal chunk carry nosequence_numberat all. Breaks clients that order/dedupe by seq.output_indexresets — forwarded upstream deltas keep their per-round index (restarts at 0), colliding with prior rounds. Live: index 3 → 0 at round 2; round-2 output overwrites round-1 at the same index.Mechanism: the hide/classify/emit logic lives in three places today (
public_output_itemsblocking,emit_gateway_*_eventsstreaming,should_hide_upstream_eventlive-hide inupstream.rs). A single GatewayAccumulator stage (the #83 sketch: Raw → GatewayAccumulator → Public) that owns the final client-visible sequence fixes all three at one seam and renumbers/dedupes consistently.Acceptance: on a ≥2-round gateway stream — exactly one
response.created, monotonicsequence_numberon every emitted frame (incl. synthetic + terminal), contiguousoutput_indexacross rounds.Coordinating with @maralbahari on this — she sketched the GatewayAccumulator shape on #83 and owns
accumulator.rs.