fix: avoid Telegram group reply thread session splits

This commit is contained in:
eliteworkstation94-ai 2026-05-19 02:17:19 +07:00 committed by Teknium
parent d69f0c1a99
commit 7b2bcba167
3 changed files with 115 additions and 16 deletions

View file

@ -4795,25 +4795,28 @@ class TelegramAdapter(BasePlatformAdapter):
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

View file

@ -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)

View file

@ -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