fix(gateway): pin Telegram DM-topic routing to user's current topic

Topic-mode DM replies were fragmenting one conversation across many sessions: a Reply on a message in another topic delivered Telegram's message_thread_id for *that* topic, and #3206's strip routed plain replies to the lobby. Both pulled the user away from their current session. Fix: when topic mode is on, rewrite source.thread_id to the user's most-recent binding if the inbound id is missing/General or not a known topic. Non-topic-mode users unchanged.
This commit is contained in:
karthikeyann
2026-05-15 16:40:15 -05:00
committed by Teknium
parent 470edfa901
commit ede47a54be
3 changed files with 185 additions and 0 deletions

View File

@@ -1814,6 +1814,54 @@ class GatewayRunner:
session_id=session_entry.session_id,
)
def _recover_telegram_topic_thread_id(
self,
source: SessionSource,
) -> 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.
"""
if (
source.platform != Platform.TELEGRAM
or source.chat_type != "dm"
or not source.chat_id
or not source.user_id
or not self._telegram_topic_mode_enabled(source)
):
return None
session_db = getattr(self, "_session_db", None)
if session_db is None:
return None
try:
bindings = session_db.list_telegram_topic_bindings_for_chat(
chat_id=str(source.chat_id),
)
except Exception:
logger.debug("topic-recover: read failed", exc_info=True)
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:
recovered = str(b.get("thread_id") or "")
if recovered and recovered != inbound:
return recovered
return None
return None
def _resolve_session_agent_runtime(
self,
*,
@@ -7498,6 +7546,21 @@ class GatewayRunner:
)
# Get or create session
# Topic-mode DMs: rewrite a stale/foreign thread_id to the user's
# last-active topic so a cross-topic Reply or stripped plain reply
# doesn't fragment the conversation across sessions.
recovered = self._recover_telegram_topic_thread_id(source)
if recovered is not None:
logger.info(
"telegram topic recovery: chat=%s user=%s %r -> %s",
source.chat_id, source.user_id, source.thread_id, recovered,
)
source = dataclasses.replace(source, thread_id=recovered)
try:
event.source = source
except Exception:
pass
session_entry = self.session_store.get_or_create_session(source)
session_key = session_entry.session_key
self._cache_session_source(session_key, source)