fix(gateway): delete partial message after fallback send on flood control (salvage of #17384)#23416
Merged
Conversation
…trol When Telegram flood control triggers 3+ consecutive edit failures, the stream consumer enters fallback mode and sends the complete response as a new message. This leaves the user seeing two messages: a frozen partial (with cursor) and the full duplicate. After the fallback chunks are sent successfully, delete the original partial message so the user only sees one complete response. The delete is best-effort — if it fails (e.g. flood still active, missing permissions), the full answer is still delivered. Fixes #16668
…on tests Follow-up to HuangYuChuh's #17384 cherry-pick: - Use defensive getattr+logger.debug for delete_message lookup, mirroring the sibling _try_send_fresh_final cleanup pattern at L820+. Platforms that don't implement delete_message no longer raise AttributeError; the failure path now logs at debug for diagnosability instead of silently swallowing. - Add three regression tests in tests/gateway/test_stream_consumer.py: - delete_message awaited on happy-path exit with stale id - delete_message NOT awaited when no fallback chunks reached the user - no crash on adapters that lack delete_message (spec-restricted mock)
Contributor
🔎 Lint report:
|
6 tasks
teknium1
pushed a commit
that referenced
this pull request
May 11, 2026
Cherry-picked from PR #10371. Two-layer defense for the spurious-thread_id issue (#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR #3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from #22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR #23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR #10371 (re-attributed from donrhmexe@ local commit author).
JZKK720
pushed a commit
to JZKK720/hermes-agent
that referenced
this pull request
May 11, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
rmulligan
pushed a commit
to rmulligan/hermes-agent
that referenced
this pull request
May 11, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
JinyuID
pushed a commit
to JinyuID/hermes-agent
that referenced
this pull request
May 11, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
02356abc
pushed a commit
to 02356abc/hermes-agent
that referenced
this pull request
May 14, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
jsboige
pushed a commit
to jsboige/hermes-agent
that referenced
this pull request
May 14, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
AlexFoxD
pushed a commit
to AlexFoxD/hermes-agent
that referenced
this pull request
May 21, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
gweeteve
pushed a commit
to gweeteve/hermes-agent
that referenced
this pull request
Jun 2, 2026
Cherry-picked from PR NousResearch#10371. Two-layer defense for the spurious-thread_id issue (NousResearch#3206): 1. _build_message_event filters DM thread_ids: only preserve thread_id for real topic messages (is_topic_message=True). Telegram puts message_thread_id on every DM that is a reply, but reply-chain ids route to nonexistent threads on send. 2. _send_message_with_thread_fallback helper: control sends (send_update_prompt, send_exec_approval / send_slash_confirm, send_model_picker) retry once without message_thread_id when Telegram returns BadRequest 'Message thread not found'. Mirrors the pattern PR NousResearch#3390 added for the streaming send path. Salvage notes: - Conflict 1 (line ~4099): merged the contributor's DM is_topic_message filter with the existing forum General-topic default from NousResearch#22423, preserving both behaviors. - Conflict 2 (line ~1664 / 1690): kept main's delete_message (PR NousResearch#23416) alongside the new helper. Tightened the helper's exception catch from bare 'Exception' to use the existing _is_bad_request_error + _is_thread_not_found_error helpers (line 484-496) for consistency with the streaming send path. - Widened the fix to send_update_prompt (was bare self._bot.send_message, same bug class). Authored by rahimsais via PR NousResearch#10371 (re-attributed from donrhmexe@ local commit author).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When Telegram throttles the bot during a long streaming reply, the stream consumer falls back to sending fresh chunks via
adapter.send(). Before this PR, the frozen partial that was being edited remained visible — users saw a stuck/cut-off message followed by a duplicate complete reply right below it.After this PR, the partial gets deleted once the full fallback response lands so users see one clean answer.
Changes
gateway/stream_consumer.py: capture the stale_message_idbefore the fallback send loop in_send_fallback_final, and on the happy-path tail calladapter.delete_message(stale_id)best-effort. Skip the delete if no chunks reached the user (the partial is the only thing they have).tests/gateway/test_stream_consumer.py: three regression tests covering happy-path delete, no-delete-on-failed-send, and adapter-without-delete_message.scripts/release.py: AUTHOR_MAP entry for HuangYuChuh.Improvements during salvage
_try_send_fresh_finalcleanup pattern at L820+: defensivegetattr(adapter, "delete_message", None)lookup +logger.debugon failure instead of bareself.adapter.delete_message+ silentexcept: pass. Platforms withoutdelete_messageno longer raise; failures now leave a diagnostic breadcrumb.Validation
tests/gateway/test_stream_consumer.py: 84/84 pass (3 new + 81 existing) in 3.9s.Closes #16668 via salvage of #17384. Original work by @HuangYuChuh; cleanup commits and tests follow.