diff --git a/gateway/run.py b/gateway/run.py index 7bf16ffc995..b9acc0bc70c 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2272,13 +2272,14 @@ class GatewayRunner: ) -> 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 @@ class GatewayRunner: 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 @@ class GatewayRunner: 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: diff --git a/tests/gateway/test_telegram_thread_fallback.py b/tests/gateway/test_telegram_thread_fallback.py index 642306c142c..6bba27a78cd 100644 --- a/tests/gateway/test_telegram_thread_fallback.py +++ b/tests/gateway/test_telegram_thread_fallback.py @@ -98,6 +98,7 @@ _fake_telegram_ext.Application = object _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") diff --git a/tests/gateway/test_telegram_topic_mode.py b/tests/gateway/test_telegram_topic_mode.py index 7945fb716b0..087edafbb61 100644 --- a/tests/gateway/test_telegram_topic_mode.py +++ b/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):