Skip to content

fix(gateway): reconcile flood-controlled partial stream instead of duplicating the final reply - #65451

Closed
AlexxRussell wants to merge 1 commit into
NousResearch:mainfrom
AlexxRussell:stream-reconcile-flood-dup
Closed

fix(gateway): reconcile flood-controlled partial stream instead of duplicating the final reply#65451
AlexxRussell wants to merge 1 commit into
NousResearch:mainfrom
AlexxRussell:stream-reconcile-flood-dup

Conversation

@AlexxRussell

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a Telegram duplicate-delivery bug in the gateway's post-stream path.

When Telegram flood control disables progressive streaming edits mid answer (the stream consumer's flood-strike limit), the consumer leaves a partial message on screen but never confirms final delivery. None of the post-stream suppression flags (streamed, previewed, content_delivered) are set, so the normal final send re-delivers the entire answer on top of the visible partial. The user sees a truncated reply, then the whole reply again once the flood-control wait expires.

This PR reconciles instead of re-sending: when the final response is ready and a partial is on screen (already_sent with a message_id) but delivery was never confirmed, the gateway edits the existing streamed message to the final content (finalize=True, which splits on overflow) and marks the response delivered. The decision predicate lives in a new pure module, gateway/stream_reconcile.should_reconcile_partial, so it is unit-testable. Any edit failure logs a warning and falls through to the existing send path, so the worst case is exactly today's behavior.

Observed live on a production gateway: a 6,375 char reply had progressive edits disabled after repeated RetryAfter strikes, the partial stayed on screen, and the final send duplicated the full answer (split, plain) roughly 3 minutes later after an 82s flood wait. With this change the partial is edited into the final reply and no duplicate is sent.

Relationship to existing work

Related Issue

None filed; root-caused from live gateway logs. Happy to open one if that helps tracking.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • gateway/stream_reconcile.py (new): should_reconcile_partial(...), a pure predicate that returns True only when a partial is on screen and delivery was never confirmed. It returns False for empty, failed, transformed, and already-delivered cases so all existing suppress/send behavior is untouched.
  • gateway/run.py: after the existing transformed-response edit branch, a guarded branch that edits the streamed partial into the final response and sets already_sent, with warning + fall-through to the normal send on any error.
  • tests/gateway/test_stream_reconcile.py (new): the reconcile case plus the negative matrix (each delivery-confirmed flag, nothing on screen, empty/failed/transformed).

How to Test

  1. python -m pytest tests/gateway/test_stream_reconcile.py -q -o addopts= (4 passed).
  2. Live repro: stream a long reply into a Telegram chat under flood control so progressive edits are disabled mid-turn (repeated RetryAfter until the consumer's strike limit). On main, the visible partial is followed by a full duplicate send after the flood wait; with this patch, the partial is edited into the final reply.

Running in production on a single-box deployment since late June; long flood-controlled replies no longer duplicate.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation: N/A (internal delivery behavior, docstrings included)
  • I've updated cli-config.yaml.example if I added/changed config keys: N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows: N/A
  • I've considered cross-platform impact (Windows, macOS): platform-agnostic gateway logic, no OS-specific code
  • I've updated tool descriptions/schemas if I changed tool behavior: N/A

…ng the final reply

When Telegram flood control disables progressive streaming edits mid
answer, the stream consumer leaves a partial message on screen but
never confirms final delivery. None of the post-stream suppression
flags (streamed, previewed, content_delivered) are set, so the normal
final send re-delivers the whole answer on top of the visible partial
and the user sees the reply twice.

Reconcile instead: when a partial is on screen (already_sent with a
message_id) and delivery was never confirmed, edit the existing
streamed message to the final content (finalize=True splits on
overflow) rather than sending a fresh duplicate. The decision lives in
gateway/stream_reconcile.should_reconcile_partial, which is pure and
unit tested; any edit failure falls through to the normal send path.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 16, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused investigation and the reproduction details.

Automated hermes-sweeper review found that current main already recovers this Telegram flood-control path at the stream-consumer layer:

  • 04898631cb72dbf84f5c066c3f87fc8c94df53a7 added final-delivery recovery after a streamed flood; 4aa499ff9f3fcc0c38ce61da46805a4dcc8f612e hardened that recovery.
  • gateway/stream_consumer.py:768-814 routes failed final edits through _send_fallback_final().
  • gateway/stream_consumer.py:957-962 computes only the unseen continuation, and :1148-1151 marks final delivery so gateway/run.py:20893-20901 suppresses the ordinary final send.
  • tests/gateway/test_telegram_final_delivery.py:27-69 covers a 180-second Telegram flood response and asserts that only the missing tail is sent.

Closing as implemented on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/telegram Telegram bot adapter sweeper:implemented-on-main Sweeper: behavior already present on current main sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants