Commit Graph

4 Commits

Author SHA1 Message Date
kshitij 68ebb198c7 fix(gateway): don't claim deleted head chunks as delivered in the empty-fallback recovery
Follow-up to #79669. That PR routed the three fallback recorder sites through
_record_turn_final_payload so a split turn would record the unsplit ledger
instead of a tail-only payload. For two of them that is right. For
_send_empty_fallback_final it is wrong, and it reintroduces the #78541 swallow
at the one site that was supposed to be fixed.

_send_empty_fallback_final is a *replacement* recovery: it sends the completed
text as a fresh message and deletes every tracked segment preview -- which on an
overflow split includes the sealed head chunks. After it runs, the only thing
on screen is the message it just sent. Recording the ledger there claims
delivery for text the same function just removed, so delivered_final_matches()
returns True, the gateway suppresses its own send, and the user is left with a
fraction of the answer.

Observed with a probe driving the real run() loop (543-char reply, 475-char head
sealed then deleted, 67-char tail committed):

  before this fix        recorded=543  matches=True   -> suppressed, 67/543 on screen
  after  this fix        recorded=67   matches=False  -> gateway sends the full answer

Record final_text verbatim here instead. The sibling site in
_send_fallback_final keeps the recorder: its delete is gated on
`continuation == final_text` and targets only the single active partial, never
the sealed heads, so the ledger correctly describes what survives.

The distinction is whether a recovery ADDS to what is on screen or REPLACES it.
Additive paths may record the ledger; replacing paths must record only what they
leave behind. _try_fresh_final is the same shape and #79669 handled it by
refusing the route on split turns.

Test drives the real seal-then-delete sequence and asserts the mismatch, so the
gateway is required to re-send. Mutation-checked: restoring the recorder call
turns it red.
2026-08-06 03:13:32 +05:30
kshitij 392e3a8c53 fix(gateway): finish the split-delivery bug class so the fix cannot duplicate or still swallow
The salvaged fix changed only the gateway's verdict: a payload-less
multi-message split stopped inheriting legacy trust. But six code paths set
_turn_split_delivery, and only one of them was taught to record a payload, so
the remaining five swapped the swallow for the opposite defect.

Fix the producers instead of only distrusting them at the boundary:

- _send_or_edit failed-final-edit branch: record the visible payload on split
  turns too. It deliberately skipped recording, which now reads as a mismatch
  and re-sends an answer already on screen -- reintroducing the duplicate
  #45517 fixed (#36965 / #25349).
- _send_fallback_final (x2) and _send_empty_fallback_final: route through
  _record_turn_final_payload instead of assigning _delivered_final_text
  directly. On a split turn their final_text is only the trailing chunk, so a
  fully delivered heads+tail reply recorded a tail-only payload and was
  re-sent in full.
- _try_fresh_final: refuse the fresh-final route once a head chunk is sealed.
  It replaces every tracked preview with one message, which only holds the
  whole answer on a single-message turn. After a split it deleted the sealed
  heads while sending just the tail, so the complete reply was still lost --
  on Telegram, the default finalize route and the shape #78541 reports.
- Set _turn_split_delivery at seal time rather than after the tail send, so
  the tail's own finalize sees the split state. The sibling overflow path
  already did this; the divergence is what let fresh-final delete the heads.
- run.py stale-finalize reconciliation: skip the in-place edit on a split
  delivery. message_id is only the LAST chunk there, so editing it with the
  complete response repeated every sealed head's text inside the tail
  message. Fall through to the normal final send.

Also drop a dead `or "".join(chunks)` fallback (all growth funnels through
_append_accumulated, so the ledger is never empty at that call site, and joined
chunks carry injected fence markers that could never match final_response), and
document that _record_turn_final_payload intentionally ignores its argument on
split turns.

Tests: four end-to-end cases driving the real overflow-split loop instead of
hand-setting private flags -- complete split still suppresses (no duplicate),
split missing a tail does not suppress, fresh-final keeps sealed heads, and a
flood-controlled final edit after a split stays suppressed. Each was
mutation-checked: reverting any individual fix turns its test red.

The pre-existing gateway-boundary test asserted the recovery *route* (the
reconcile edit) rather than the guarantee. Relaxed to the real contract: either
_run_agent puts the complete text on the wire, or it declines to claim delivery
so the caller's normal final send does.

Co-authored-by: HexLab98 <liruixinch@outlook.com>
2026-08-06 02:54:32 +05:30
HexLab98 a2ca5c2a78 test(gateway): cover payload-less split-delivery final-send swallow
Add unit and GatewayRunner boundary coverage for the #78541 shape where
final_content_delivered is set via split delivery with no recorded
payload.
2026-08-06 02:54:32 +05:30
Teknium 30878411b8 fix(gateway): stop stale streamed finalize from suppressing the complete Telegram response
A successful finalize edit can carry only the last streamed preview
snapshot: deltas generated between the last preview edit and stream
completion never reach any Bot API call, yet final_response_sent /
final_content_delivered were set from the call's success and suppressed
the gateway's normal final send — losing the tail permanently.

The stream consumer now records the exact cleaned payload of every
turn-final delivery (delivered_final_matches tri-state), and gateway/run.py
reconciles that record against the completed final_response before
trusting either suppression flag. On a demonstrable mismatch it edits the
streamed message up to the complete response, falling back to the normal
final send if the edit fails. Multi-message split deliveries and legacy
paths without a record keep the existing flag-trusting behavior, so
overflow splits and the failed-finalize handling (#51828/#33793) are
untouched.

Fixes #71643
2026-08-01 10:51:55 -07:00