fix(telegram): preserve new DM topic lanes

This commit is contained in:
Maxim Esipov 2026-05-19 09:58:38 +03:00 committed by Teknium
parent eea9553a9c
commit bdc9b0eff5
3 changed files with 22 additions and 15 deletions

View file

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

View file

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

View file

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