Telegram thread ID session-splitting bug fixes

NousResearch/hermes-agent · cluster 13195 · gateway

Data as of

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

The short answer

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

These issues and pull requests address a bug where Telegram group messages carrying message_thread_id values cause unexpected session splits or misrouted human-handoff replies. Fixes center on normalizing thread IDs through a shared helper to prevent incorrect session routing.

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.

  • #28506 fix: avoid Telegram group reply thread session splits

    merged to main

    files: gateway/platforms/telegram.py, scripts/release.py, tests/gateway/test_dm_topics.py, tests/gateway/test_telegram_thread_fallback.py

    Show the diff
    • gateway/platforms/telegram.py
      @@ -4795,25 +4795,28 @@ def _build_message_event(
               elif chat.type == ChatType.CHANNEL:
                   chat_type = "channel"
       
      -        # Resolve DM topic name and skill binding.
      -        # In private chats, only preserve thread ids for real topic messages
      -        # (is_topic_message=True).  Telegram puts message_thread_id on every
      -        # DM that is a reply, even when the user is just replying to a
      -        # previous message in the same DM — that bogus id then routes to a
      -        # nonexistent thread and Telegram returns 'Message thread not found'
      -        # on send (#3206).
      +        # Resolve Telegram topic name and skill binding.
      +        # Only preserve message_thread_id when Telegram marks the message as
      +        # a real topic/forum message. Telegram can also populate
      +        # message_thread_id for ordinary reply UI anchors; treating those as
      +        # durable session threads fragments workflows such as CAPTCHA/login
      +        # handoffs where the user later replies "done" in the same group.
      +        # Private chats have the same pitfall: only real DM topic messages
      +        # (is_topic_message=True) should keep the thread id, otherwise sends
      +        # can hit Telegram's 'Message thread not found' error (#3206).
               thread_id_raw = message.message_thread_id
               is_topic_message = bool(getattr(message, "is_topic_message", False))
      +        is_forum_group = getattr(chat, "is_forum", False) is True
               thread_id_str = None
               if thread_id_raw is not None:
      -            if chat_type == "group":
      +            if chat_type == "group" and (is_topic_message or is_forum_group):
                       thread_id_str = str(thread_id_raw)
                   elif chat_type == "dm" and is_topic_message:
                       thread_id_str = str(thread_id_raw)
               # For forum groups without an explicit topic, default to the
               # General-topic id so the gateway routes back to the General topic
               # rather than dropping into the bot's main channel (#22423).
      -        if chat_type == "group" and thread_id_str is None and getattr(chat, "is_forum", False):
      +        if chat_type == "group" and thread_id_str is None and is_forum_group:
                   thread_id_str = self._GENERAL_TOPIC_THREAD_ID
               chat_topic = None
               topic_skill = None
    • scripts/release.py
      @@ -171,6 +171,7 @@
           "[redacted:email]": "fabiosiqueira",  # PR #27212 salvage (bg-process notif anchor)
           "[redacted:email]": "falconexe",  # PR #24511 salvage (sticky-IP reset)
           "[redacted:email]": "fonhal",  # PR #27865/#27861 salvage (mention entities / typing fallback)
      +    "[redacted:email]": "eliteworkstation94-ai",  # PR #28157 salvage (group reply session splits)
           "[redacted:email]": "hllqkb",
           "[redacted:email]": "Bongulielmi",
           "[redacted:email]": "JTroyerOvermatch",
    • tests/gateway/test_dm_topics.py
      @@ -449,13 +449,15 @@ def test_cache_dm_topic_from_message_no_overwrite():
       
       def _make_mock_message(chat_id=111, chat_type="private", text="hello", thread_id=None,
                              user_id=42, user_name="Test User", forum_topic_created=None,
      -                       is_topic_message=None):
      +                       is_topic_message=None, is_forum=None):
           """Create a mock Telegram Message for _build_message_event tests."""
           chat = SimpleNamespace(
               id=chat_id,
               type=chat_type,
               title=None,
           )
      +    if is_forum is not None:
      +        chat.is_forum = is_forum
           # Add full_name attribute for DM chats
           if not hasattr(chat, "full_name"):
               chat.full_name = user_name
      @@ -594,7 +596,12 @@ def test_group_topic_skill_binding():
           ])
       
           msg = _make_mock_message(
      -        chat_id=-1001234567890, chat_type=_ChatType.SUPERGROUP, thread_id=5, text="hello"
      +        chat_id=-1001234567890,
      +        chat_type=_ChatType.SUPERGROUP,
      +        thread_id=5,
      +        text="hello",
      +        is_topic_message=True,
      +        is_forum=True,
           )
           event = adapter._build_message_event(msg, MessageType.TEXT)
       
      @@ -617,7 +624,12 @@ def test_group_topic_skill_binding_second_topic():
           ])
       
           msg = _make_mock_message(
      -        chat_id=-1001234567890, chat_type=_ChatType.SUPERGROUP, thread_id=12, text="deal update"
      +        chat_id=-1001234567890,
      +        chat_type=_ChatType.SUPERGROUP,
      +        thread_id=12,
      +        text="deal update",
      +        is_topic_message=True,
      +        is_forum=True,
           )
           event = adapter._build_message_event(msg, MessageType.TEXT)
       
      @@ -639,7 +651,12 @@ def test_group_topic_no_skill_binding():
           ])
       
           msg = _make_mock_message(
      -        chat_id=-1001234567890, chat_type=_ChatType.SUPERGROUP, thread_id=1, text="hey"
      +        chat_id=-1001234567890,
      +        chat_type=_ChatType.SUPERGROUP,
      +        thread_id=1,
      +        text="hey",
      +        is_topic_message=True,
      +        is_forum=True,
           )
           event = adapter._build_message_event(msg, MessageType.TEXT)
       
      @@ -661,7 +678,12 @@ def test_group_topic_unmapped_thread_id():
           ])
       
           msg = _make_mock_message(
      -        chat_id=-1001234567890, chat_type=_ChatType.SUPERGROUP, thread_id=999, text="random"
      +        chat_id=-1001234567890,
      +        chat_type=_ChatType.SUPERGROUP,
      +        thread_id=999,
      +        text="random",
      +        is_topic_message=True,
      +        is_forum=True,
           )
           event = adapter._build_message_event(msg, MessageType.TEXT)
       
      @@ -683,7 +705,12 @@ def test_group_topic_unmapped_chat_id():
           ])
       
           msg = _make_mock_message(
      -        chat_id=-1009999999999, chat_type=_ChatType.SUPERGROUP, thread_id=5, text="wrong group"
      +        chat_id=-1009999999999,
      +        chat_type=_ChatType.SUPERGROUP,
      +        thread_id=5,
      +        text="wrong group",
      +        is_topic_message=True,
      +        is_forum=True,
           )
           event = adapter._build_message_event(msg, MessageType.TEXT)
       
      @@ -720,7 +747,12 @@ def test_group_topic_chat_id_int_string_coercion():
           ])
       
           msg = _make_mock_message(
      -        chat_id=-1001234567890, chat_type=_ChatType.SUPERGROUP, thread_id=7, text="test"
      +        chat_id=-1001234567890,
      +        chat_type=_ChatType.SUPERGROUP,
      +        thread_id=7,
      +        text="test",
      +        is_topic_message=True,
      +        is_forum=True,
           )
           event = adapter._build_message_event(msg, MessageType.TEXT)
       
    • tests/gateway/test_telegram_thread_fallback.py
      @@ -134,6 +134,70 @@ def _make_adapter():
           return adapter
       
       
      +def test_non_forum_group_reply_thread_id_does_not_fork_session_key():
      +    """Reply-derived thread ids in ordinary groups must not create topic lanes."""
      +    from gateway.platforms import telegram as telegram_mod
      +
      +    adapter = _make_adapter()
      +    message = SimpleNamespace(
      +        text="Done",
      +        caption=None,
      +        chat=SimpleNamespace(
      +            id=-100123,
      +            type=telegram_mod.ChatType.SUPERGROUP,
      +            is_forum=False,
      +            title="Regular group",
      +        ),
      +        from_user=SimpleNamespace(id=456, full_name="Alice"),
      +        message_thread_id=461,
      +        is_topic_message=False,
      +        reply_to_message=SimpleNamespace(
      +            message_id=460,
      +            text="Please complete the CAPTCHA/login, then reply done.",
      +            caption=None,
      +        ),
      +        message_id=462,
      +        date=None,
      +    )
      +
      +    event = adapter._build_message_event(message, msg_type=MessageType.TEXT)
      +
      +    assert event.source.chat_id == "-100123"
      +    assert event.source.chat_type == "group"
      +    assert event.source.thread_id is None
      +    assert build_session_key(event.source) == "agent:main:telegram:group:-100123:456"
      +
      +
      +def test_forum_group_topic_message_preserves_thread_session_key():
      +    """Real Telegram forum-topic messages should still route by topic id."""
      +    from gateway.platforms import telegram as telegram_mod
      +
      +    adapter = _make_adapter()
      +    message = SimpleNamespace(
      +        text="hello from topic",
      +        caption=None,
      +        chat=SimpleNamespace(
      +            id=-100123,
      +            type=telegram_mod.ChatType.SUPERGROUP,
      +            is_forum=True,
      +            title="Forum group",
      +        ),
      +        from_user=SimpleNamespace(id=456, full_name="Alice"),
      +        message_thread_id=17585,
      +        is_topic_message=True,
      +        reply_to_message=None,
      +        message_id=10,
      +        date=None,
      +    )
      +
      +    event = adapter._build_message_event(message, msg_type=MessageType.TEXT)
      +
      +    assert event.source.chat_id == "-100123"
      +    assert event.source.chat_type == "group"
      +    assert event.source.thread_id == "17585"
      +    assert build_session_key(event.source) == "agent:main:telegram:group:-100123:17585"
      +
      +
       def test_forum_general_topic_without_message_thread_id_keeps_thread_context():
           """Forum General-topic messages should keep synthetic thread context."""
           from gateway.platforms import telegram as telegram_mod

2 PRs closed without merging

From the close comments: merged via another change 1 · superseded by another PR 1

  • #13338 fix(telegram): avoid unexpected thread session splits · superseded by another PR → 7b2bcba, #13195
  • #28157 fix: avoid Telegram group reply thread session splits · merged via another change → #28506
Closed issues · 2
  • Closed#13195Telegram groups can split into unexpected sessions when inbound messages carry message_thread_id without user-created topics
  • Closed#28153Telegram group reply-derived thread IDs can route human-handoff “done” replies into the wrong session
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.34: 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
#28506
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
19 May 2026

For your agent

The same facts, typed.