Skip to content

fix(telegram): preserve DM topic routing via reply fallback (salvage #22053) - #22410

Merged
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/telegram-dm-topic-22053
May 9, 2026
Merged

fix(telegram): preserve DM topic routing via reply fallback (salvage #22053)#22410
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/telegram-dm-topic-22053

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Salvage of #22053 by @leehack onto current main, fixing #22022 (Telegram Bot API 10.0 broke private-chat topic replies — Hermes responses landed in "All Messages" instead of the active DM topic lane).

Root cause (from #22022)

Bot API 10.0 changed how Telegram routes outbound sends in private-chat topic lanes. Previously, sendMessage(message_thread_id=N) worked. After 10.0, the same call fails with Bad Request: message thread not found. Hermes' existing fallback retried without the thread id, dropping the response into the root chat.

Fix (from #22053, by @leehack)

Three distinct routing modes:

  1. Forum / supergroup topics — keep using message_thread_id (unchanged).
  2. True Bot API Direct Messages topics — explicit direct_messages_topic_id metadata opt-in.
  3. Hermes-created private DM topic lanes — reply-anchor fallback (send message_thread_id together with a reply_to_message_id pointing at the triggering user message; if either is missing, drop both rather than land in the wrong lane).

Threaded through new _thread_metadata_for_source(source, reply_to_message_id) and _reply_anchor_for_event(event) helpers in gateway/platforms/base.py, then wired into ~15 outgoing send sites in gateway/run.py and all media/keyboard sends in gateway/platforms/telegram.py.

Follow-up fix (added during salvage review)

send_typing was the one outgoing site #22053 didn't update because Telegram's send_chat_action only accepts message_thread_id (no direct_messages_topic_id equivalent). Without a fix, every typing tick (~every 2s during agent runs) makes a doomed API call that gets logged at debug level. This salvage adds a short-circuit: when metadata indicates a DM topic reply-fallback lane, skip the call entirely. User-visible behavior is unchanged (no typing indicator either way for these lanes), but logs stay clean.

Verification

bash scripts/run_tests.sh \
  tests/gateway/test_telegram_thread_fallback.py \
  tests/gateway/test_run_progress_topics.py \
  tests/gateway/test_background_command.py \
  tests/gateway/test_voice_command.py \
  tests/gateway/test_discord_free_response.py \
  tests/gateway/test_feishu.py
=> 480 passed

Lint diff (scripts/lint_diff.py): 🆕 New issues: none for both ruff and ty.

Closes / supersedes

Attribution

Commit Author Description
2065cd583 @leehack fix(telegram): preserve DM topic routing via reply fallback
142013a54 @kshitijk4poor fix(telegram): skip send_chat_action for DM topic reply-fallback lanes

@leehack's email (jhin.lee@unity3d.com) is added to AUTHOR_MAP in #22409 (prerequisite, must merge first).

Test plan for review

  1. Merge chore(release): add leehack to AUTHOR_MAP for PR #22053 salvage #22409 first (AUTHOR_MAP).
  2. Read the diff: 7 files changed, 1370 insertions, 153 deletions.
  3. Run focused tests as above.
  4. (Optional) Live-test on a real Telegram bot in a private DM topic lane — message the bot in a topic, verify the response stays in the topic lane instead of leaking to "All Messages."

leehack and others added 2 commits May 9, 2026 13:59
The send path uses Hermes' reply-anchor fallback for DM topic lanes
(message_thread_id + reply_to_message_id), but send_chat_action only
accepts message_thread_id — Telegram's Bot API 10.0 rejects it for
these lanes. Without this short-circuit, every typing tick (~every 2s
during agent runs) makes a doomed API call that gets logged as a
'thread not found' debug warning. Skip the call entirely when the
metadata indicates a DM topic reply-fallback lane; the user-visible
behavior is unchanged (no typing indicator either way for these
lanes), but the logs stay clean.

Identified during salvage review of NousResearch#22053.
@kshitijk4poor
kshitijk4poor merged commit aef297a into NousResearch:main May 9, 2026
8 of 11 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/telegram-dm-topic-22053 branch May 9, 2026 08:40
kshitijk4poor added a commit that referenced this pull request Jun 21, 2026
…er (#22773)

PR #22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case #22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from #42051 (stepanov1975) and #23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
kpadilha pushed a commit to kpadilha/hermes-agent that referenced this pull request Jun 24, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
pai-scaffolde pushed a commit to pai-scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
indigokarasu pushed a commit to indigokarasu/hermes-agent that referenced this pull request Jul 1, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…er (NousResearch#22773)

PR NousResearch#22410 added three-mode Telegram topic routing to the live message path
(TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery
path never got it. cron/scheduler.py::_deliver_result sent through the live
adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone
_send_telegram, neither of which addresses Bot API Direct Messages topics
correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a
bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a
private DM topic landed in the General topic instead of the requested lane.

Fix: the cron live-adapter branch now routes the text send through the
gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path
live messages use — so it inherits all three Telegram routing modes:

  1. Forum/supergroup (negative chat_id) -> message_thread_id
  2. Bot API DM topics (private chat_id + numeric topic id) ->
     direct_messages_topic_id  (the case NousResearch#22773 reported)
  3. Hermes-created named private DM-topic lanes -> ensure_dm_topic +
     reply anchor

For mode 2, a private-chat target with a numeric topic id is passed as
``direct_messages_topic_id`` metadata (verified end-to-end:
TelegramAdapter._thread_kwargs_for_send turns it into
``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a
bare message_thread_id. Forum/supergroup and home-channel deliveries are
unchanged. The standalone fallback (gateway down) is preserved.

No new config knob and no duplicated routing logic — this reuses the existing
DeliveryRouter rather than reimplementing topic routing in the cron path.

Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both
diagnosed the missing three-mode routing in the cron/standalone path;
reimplemented onto the canonical DeliveryRouter that landed since those PRs
were opened.

Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com>
Co-authored-by: devsart95 <devsart95@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants