Telegram DM topic session isolation and recovery bugs

NousResearch/hermes-agent · cluster 30411 · gateway

Data as of

Nothing open on GitHub. Fix merged in #31444. Check it covers your case.

The short answer

0open issues
0open PRs
11items grouped
Known?
Yes. 11 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #31444. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #31444. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple bug reports and fix attempts addressing a function that incorrectly collapses or hijacks new Telegram DM topic sessions into previous ones. The cluster centers on preserving parallel DM topic lane isolation during topic recovery.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #31444 fix(gateway): preserve new Telegram DM topic lanes

    merged to main

    files: gateway/run.py, tests/gateway/test_telegram_thread_fallback.py, tests/gateway/test_telegram_topic_mode.py

    Show the diff
    • gateway/run.py
      @@ -2272,13 +2272,14 @@ def _recover_telegram_topic_thread_id(
           ) -> Optional[str]:
               """Pin DM-topic routing to the user's last-active topic.
       
      -        Telegram fragments topic-mode DMs two ways: a Reply on a message
      -        in another topic delivers ``message_thread_id`` for *that* topic,
      -        and ``_build_message_event`` strips the thread_id on plain replies
      -        (#3206 — needed for non-topic users). Both route the user to the
      -        wrong session. When topic mode is on, rewrite the thread_id to the
      -        user's most-recent binding if the inbound id is missing/General or
      -        not a known topic for this chat. Returns None to leave it alone.
      +        Telegram can omit ``message_thread_id`` or surface General (``1``)
      +        for some topic-mode DM replies. In those lobby-shaped cases, keep the
      +        conversation attached to the user's most-recent bound topic.
      +
      +        Do not rewrite a non-lobby, previously-unbound thread id: a newly
      +        created Telegram DM topic is also "unknown" until the first inbound
      +        message is recorded, and rewriting it would send that brand-new topic's
      +        answer into an older lane. Returns None to leave the source alone.
               """
               if (
                   source.platform != Platform.TELEGRAM
      @@ -2288,6 +2289,14 @@ def _recover_telegram_topic_thread_id(
                   or not self._telegram_topic_mode_enabled(source)
               ):
                   return None
      +        inbound = str(source.thread_id or "")
      +        is_lobby = not inbound or inbound in self._TELEGRAM_GENERAL_TOPIC_IDS
      +        if not is_lobby:
      +            # A non-lobby, unknown thread_id is most likely the first message in
      +            # a brand-new Telegram DM topic. Preserve it so it can be recorded
      +            # as a new independent lane below instead of hijacking the latest
      +            # existing topic binding.
      +            return None
               session_db = getattr(self, "_session_db", None)
               if session_db is None:
                   return None
      @@ -2300,11 +2309,6 @@ def _recover_telegram_topic_thread_id(
                   return None
               if not bindings:
                   return None
      -        inbound = str(source.thread_id or "")
      -        is_lobby = not inbound or inbound in self._TELEGRAM_GENERAL_TOPIC_IDS
      -        known = {str(b.get("thread_id") or "") for b in bindings}
      -        if not is_lobby and inbound in known:
      -            return None
               user_id = str(source.user_id)
               for b in bindings:  # newest-first
                   if str(b.get("user_id") or "") == user_id:
    • tests/gateway/test_telegram_thread_fallback.py
      @@ -98,6 +98,7 @@ def __init__(self, media, caption=None, **kwargs):
       _fake_telegram_ext.CommandHandler = object
       _fake_telegram_ext.CallbackQueryHandler = object
       _fake_telegram_ext.MessageHandler = object
      +_fake_telegram_ext.TypeHandler = object
       _fake_telegram_ext.ContextTypes = SimpleNamespace(DEFAULT_TYPE=object)
       _fake_telegram_ext.filters = object
       _fake_telegram_request = types.ModuleType("telegram.request")
    • tests/gateway/test_telegram_topic_mode.py
      @@ -1175,13 +1175,15 @@ def test_recover_returns_none_for_known_topic(tmp_path):
           assert runner._recover_telegram_topic_thread_id(_make_source(thread_id="222")) is None
       
       
      -def test_recover_rewrites_unknown_thread_id_to_most_recent(tmp_path):
      -    # Cross-topic Reply leak: inbound thread_id is a Telegram-only id we never bound.
      +def test_recover_preserves_unknown_thread_id_for_new_topic(tmp_path):
      +    # A newly-created Telegram DM topic arrives with a real, previously-unbound
      +    # message_thread_id. It must become its own session lane rather than being
      +    # rewritten to whichever older topic was most recently active.
           db = SessionDB(db_path=tmp_path / "state.db")
           _seed_two_topic_bindings(db)
           runner = _make_runner(session_db=db)
       
      -    assert runner._recover_telegram_topic_thread_id(_make_source(thread_id="9999")) == "222"
      +    assert runner._recover_telegram_topic_thread_id(_make_source(thread_id="9999")) is None
       
       
       def test_recover_rewrites_lobby_thread_id_to_most_recent(tmp_path):
      @@ -1209,6 +1211,31 @@ def test_recover_returns_none_when_no_bindings_yet(tmp_path):
           assert runner._recover_telegram_topic_thread_id(_make_source(thread_id=None)) is None
       
       
      +def test_recover_returns_none_for_brand_new_topic(tmp_path):
      +    # Regression for #31086: bindings exist for a prior topic but the user
      +    # opened a fresh one (thread_id "99999"). Recovery must return None so the
      +    # new topic gets its own session rather than being silently merged into
      +    # the previous topic's session. The hijack was self-reinforcing — because
      +    # the rewrite ran before _record_telegram_topic_binding, the new topic's
      +    # binding row never got written, so every subsequent message in that topic
      +    # looked "unknown" and was hijacked again.
      +    db = SessionDB(db_path=tmp_path / "state.db")
      +    db.enable_telegram_topic_mode(chat_id="208214988", user_id="208214988")
      +    db.create_session(session_id="sess-old", source="telegram", user_id="208214988")
      +    src_old = _make_source(thread_id="12345")
      +    db.bind_telegram_topic(
      +        chat_id=src_old.chat_id,
      +        thread_id=src_old.thread_id,
      +        user_id=src_old.user_id,
      +        session_key=build_session_key(src_old),
      +        session_id="sess-old",
      +    )
      +    runner = _make_runner(session_db=db)
      +
      +    # "99999" is non-lobby and not in the binding table — brand-new topic.
      +    assert runner._recover_telegram_topic_thread_id(_make_source(thread_id="99999")) is None
      +
      +
       def test_list_telegram_topic_bindings_for_chat(tmp_path):
           db = SessionDB(db_path=tmp_path / "state.db")
           _seed_two_topic_bindings(db)

7 PRs closed without merging

From the close comments: superseded by another PR 3 · duplicate 2 · PR volume cleanup 1 · merged via another change 1

  • #28605 fix(telegram): preserve new DM topic lanes · merged via another change → 14ad7b2, #31444, #31086
  • #29287 fix(gateway): don't hijack brand-new Telegram DM topics into previous session · superseded by another PR → #31444, #31086
  • #29546 fix: new Telegram DM topics from All Messages reroute to previous session · superseded by another PR → #31444, #31086
  • #30422 fix(gateway): make Telegram root-DM topic pinning opt-in (#30411) · duplicate → #30411, #30422, #17172
  • #31088 fix(gateway): don't hijack brand-new Telegram DM topics into the previous topic · superseded by another PR → #31444, #31086
Show 2 more
  • #31812 fix(gateway): keep parallel Telegram DM topic sessions from collapsing · duplicate → #30538, #30411, #28605, #30422, #29794
  • #40525 fix(gateway): restore Telegram DM topic isolation · PR volume cleanup
Closed issues · 3
  • Closed#30411[Bug]: Telegram DM auto-topic-rename and tool-call routing broken by _recover_telegram_topic_thread_id()
  • Closed#30538fix(gateway): DM topic recovery collapses parallel sessions into one
  • Closed#31086[Bug] _recover_telegram_topic_thread_id hijacks every brand-new Telegram DM topic into the previous topic
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.18: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#31444
Closed issues
3
Attempts
7 PRs closed without merging
Fix link checked
24 May 2026

For your agent

The same facts, typed.