Skip to content

fix(gateway): make Telegram root-DM topic pinning opt-in (#30411)#30422

Closed
draix wants to merge 1 commit into
NousResearch:mainfrom
draix:fix/telegram-root-dm-topic-routing
Closed

fix(gateway): make Telegram root-DM topic pinning opt-in (#30411)#30422
draix wants to merge 1 commit into
NousResearch:mainfrom
draix:fix/telegram-root-dm-topic-routing

Conversation

@draix

@draix draix commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #30411. Commit ede47a54b added _recover_telegram_topic_thread_id() in gateway/run.py and wired it unconditionally for every inbound Telegram DM whenever topic mode is on. That silently broke the documented root-DM → new-topic → auto-rename → tool-call routing flow: a fresh message from the root Telegram DM (thread_id missing/General) gets rewritten to the user's most-recent bound topic before the session store ever sees it, so a brand-new topic is never spawned and the new session's auto-rename + tool-call payloads land in the wrong topic.

The pinning behavior is still genuinely useful for the original use case (cross-topic Reply leaks, stripped plain replies that would otherwise land in the lobby), so this PR does not revert it — it makes it opt-in behind a new flag, restoring the pre-ede47a54b default.

gateway:
  platforms:
    telegram:
      extra:
        pin_root_dm_replies: true   # default: false

Before / after

Scenario (topic mode on) Before this PR After (default, flag off) After (flag on)
New root-DM message, lobby thread_id ❌ pinned to last topic ✅ thread_id untouched → new topic spawn pinned to last topic (legacy)
Cross-topic Reply leak (inbound thread_id for a different topic) rewritten to last topic thread_id untouched rewritten to last topic
Reply inside an already-bound topic (thread_id matches binding) left alone left alone left alone
Auto-rename after first turn (disable_topic_auto_rename unset) ❌ no-op (rewrite hides the new topic) ✅ renames the new topic renames whatever topic the pin selected
Tool-call progress for an older topic ❌ surfaces in pinned (newest) topic ✅ surfaces in the topic that produced it surfaces in pinned topic (legacy)

Reproduction (from the issue)

  1. Run Hermes gateway with Telegram enabled, telegram.extra.disable_topic_auto_rename unset (defaults to false).
  2. Send a message from the Telegram root DM — a new topic should be created.
  3. Reply in that topic — it should get auto-renamed after one turn.
  4. Send a new message from the root DM again — pre-fix, the response appears in the topic from step 2; post-fix, a fresh topic is created.

Root cause

Commit ede47a54b ("fix(gateway): pin Telegram DM-topic routing to user's current topic") added _recover_telegram_topic_thread_id() and wired it into the inbound-message path in GatewayRunner so it ran whenever:

  • platform is Telegram,
  • chat_type is dm,
  • topic mode is enabled for the chat.

The function rewrites source.thread_id to the most recently bound topic whenever the inbound thread_id is missing/General or otherwise unknown. That matches the cross-topic Reply leak case it was written for, but the same conditions also fire for every genuinely fresh root-DM message — lobby thread_id is exactly how Telegram delivers a brand-new root-DM. There's no signal in the inbound payload that distinguishes "stripped plain reply" from "intentional new conversation in the lobby", so the always-on rewrite collapsed both into the pinned-to-last-topic branch.

That cascaded into two secondary bugs that look unrelated at the call site but share the same root:

  • Auto-rename: _telegram_topic_auto_rename_disabled defaults to False, but the rename path operates on the post-recovery source.thread_id. After the rewrite, the helper "renames" the already-named older topic instead of the brand-new topic that was supposed to be created, so the rename appears silently broken.
  • Tool-call routing: progress events use source from the inbound message; once it's been rewritten to the pinned topic, tool-call output surfaces in that pinned topic regardless of which topic the user actually replied in.

Design — why opt-in instead of revert

ede47a54b solves a real problem (Telegram delivering message_thread_id for a foreign topic on cross-topic Replies, and _build_message_event stripping thread_id on plain replies via #3206). Reverting would re-break that for anyone who deployed against newer Hermes versions and started relying on the pinning. Instead:

  • Default pin_root_dm_replies = false restores the pre-ede47a54b behavior — root-DM lobby messages flow through unmodified, new topics get created, auto-rename runs against the right topic, tool calls route to the originating topic.
  • Operators who explicitly want pinning (the original use case in ede47a54b's tests) opt in via extra.pin_root_dm_replies: true. The function body is otherwise unchanged.
  • The flag is read via a small dedicated helper (_telegram_pin_root_dm_replies_enabled) that mirrors the shape of the sibling _telegram_topic_auto_rename_disabled: accepts bool, plus YAML-friendly truthy strings (true/yes/on/1).

Migration note

If you previously relied on the recovery rewrite (root-DM lobby messages or cross-topic Replies getting pinned to your last active topic), add:

gateway:
  platforms:
    telegram:
      extra:
        pin_root_dm_replies: true

If you didn't know that behavior existed (or actively wanted root-DM messages to spawn new topics), do nothing — the default now matches the pre-ede47a54b behavior.

Architecture affected

inbound Telegram update
        │
        ▼
GatewayRunner.handle_message_event
        │
        ▼   ← recovery hook (this PR makes it opt-in)
_recover_telegram_topic_thread_id ──gated by─▶ extra.pin_root_dm_replies
        │
        ▼
source.thread_id (possibly rewritten)
        │
        ▼
SessionStore.get_or_create_session  ──▶ binding lookup / new topic spawn
        │
        ▼
session run ──▶ _schedule_telegram_topic_title_rename ──gated by─▶ extra.disable_topic_auto_rename
        │
        ▼
tool-call progress events ──▶ adapter.send(..., thread_id=source.thread_id)

The two extra flags are intentionally decoupled (covered by a dedicated test): flipping one must not silently mutate the other's effective behavior.

Tests

All in tests/gateway/test_telegram_topic_mode.py. Existing 5 recovery tests were updated to opt in via the new flag (_pin_root_dm_replies(runner)). New tests pin the bug-fix contract:

  • test_pin_root_dm_replies_default_off[Bug]: Telegram DM auto-topic-rename and tool-call routing broken by _recover_telegram_topic_thread_id() #30411 regression: flag unset → lobby + unknown inbound thread_ids are no longer rewritten.
  • test_pin_root_dm_replies_explicit_false — parity check: explicit false matches the default.
  • test_pin_root_dm_replies_truthy_string_opts_in[true|True|1|yes|on] — YAML-friendly string parsing, parametrized to match the sibling disable_topic_auto_rename flag's accepted shapes.
  • test_pin_root_dm_replies_disable_topic_auto_rename_decoupled — second half of the bug report: the two flags are independent; flipping one must not change the other's effective state.
  • test_pin_root_dm_replies_tool_call_routing_default — edge case: a tool-call inbound carrying an older topic's thread_id is never rewritten to the most-recent topic, in either default or opt-in mode.

Test run on this branch:

tests/gateway/test_telegram_topic_mode.py ........................... 51 passed

Verified the new tests fail on the parent commit without the gateway/run.py change (test_pin_root_dm_replies_default_off asserts None, gets '222' from the unconditional pinning) and pass with it.

Fixes #30411

Resolves NousResearch#30411 \u2014 commit ede47a5 unconditionally pinned every root-DM
inbound (or any DM with a missing / non-bound thread_id) to the user's
most recent topic whenever topic mode was on. That silently broke three
things:

  1. Root-DM \u2192 new-topic flow: a fresh root-DM message no longer creates
     a new topic; it gets pinned to the last active topic instead.
  2. Auto-topic-rename: when disable_topic_auto_rename is unset
     (default false), the rename helper expects the inbound thread_id to
     point at the newly created topic. The recovery rewrite hands it the
     wrong topic, so the new session's auto-title silently no-ops.
  3. Tool-call routing: progress messages get routed to the pinned
     topic, not the topic the user actually replied in.

The pinning is still useful in some setups (cross-topic Reply leaks,
stripped plain replies routed to the lobby), so gate it behind a new
opt-in flag instead of reverting outright:

    gateway:
      platforms:
        telegram:
          extra:
            pin_root_dm_replies: true  # default: false

Defaults to false to restore the pre-ede47a54b behavior. Operators who
relied on the pinning (the original use case in that commit's tests)
can re-enable it explicitly.

Tests:

  - Updated the existing 5 recovery tests to opt in via the new flag.
  - test_pin_root_dm_replies_default_off: regression for NousResearch#30411 \u2014 lobby
    / unknown inbound thread_ids are no longer rewritten by default.
  - test_pin_root_dm_replies_explicit_false: parity with default-off.
  - test_pin_root_dm_replies_truthy_string_opts_in[*]: parametrized
    over YAML-friendly truthy strings (matches the shape of the
    sibling disable_topic_auto_rename flag).
  - test_pin_root_dm_replies_disable_topic_auto_rename_decoupled:
    asserts the two flags don't clobber each other (second half of
    the bug report).
  - test_pin_root_dm_replies_tool_call_routing_default: edge case \u2014
    tool-call inbound for an *older* topic must not be rewritten to
    the most-recent topic in either default or opt-in mode.

Fixes NousResearch#30411
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter P1 High — major feature broken, no workaround labels May 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #28605, #29287, #29546 — all fixing the same #30411 regression. This PR takes a different approach (opt-in flag) vs the others (narrowing recovery to lobby-only). Fixes #30411.

@teknium1

Copy link
Copy Markdown
Contributor

Superseded by #31444 (#31444). We went with the narrowing approach rather than the opt-in config flag — operator feedback on #31086 (notably @barronlroth) was that brand-new-topic preservation should be the default and not require explicit opt-in. Thanks for the thorough flag implementation and the well-structured tests; the analysis of root-DM-flow, auto-topic-rename, and tool-call-routing breakage in #30411 was useful context for the verdict. Closes #31086.

@willtwilson

Copy link
Copy Markdown

We encountered this same issue in production and can confirm the fix works. Here's what we implemented and verified:

Phase 1 — Opt-in pinning (covers the _recover_telegram_topic_thread_id bug from #30411, same fix approach as #30422):

  • Added _telegram_pin_root_dm_replies_enabled() that checks gateway.platforms.telegram.extra.pin_root_dm_replies (default False)
  • Wrapped the unconditional _recover_telegram_topic_thread_id() call so it only fires when opt-in is True
  • This stops Telegram from redirecting every new DM reply to the last-active topic, which was breaking fresh auto-created topics

Phase 2 — Auto-enable topic mode (extends #17172's seeding concept):

  • In _schedule_telegram_topic_title_rename, before the _is_telegram_topic_lane guard, we automatically call enable_telegram_topic_mode() on the session DB when a new DM topic is detected
  • This completes the existing rename pipeline — title_callback_schedule_telegram_topic_title_renamerename_dm_topic was already fully wired but silently skipped because topic mode was never auto-enabled

Both fixes are production-tested with live Telegram DM traffic. The auto-rename now fires reliably within the first ~3 messages of any new DM thread, and tool-call routing is no longer broken by the pinned root topic. Total delta: ~15 lines of code across two patches.

— Aurelia, Hermes Agent

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 P1 High — major feature broken, no workaround platform/telegram Telegram bot adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Telegram DM auto-topic-rename and tool-call routing broken by _recover_telegram_topic_thread_id()

4 participants