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
- 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
Open PRs0
Fixes on record
-
#28506 fix: avoid Telegram group reply thread session splits
files:
gateway/platforms/telegram.py,scripts/release.py,tests/gateway/test_dm_topics.py,tests/gateway/test_telegram_thread_fallback.pyShow 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
Closed issues · 2
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.00means 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 LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #28506
- Closed issues
- 2
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 19 May 2026