Skip to content

Commit 142a575

Browse files
xxxigmteknium1
authored andcommitted
gateway/telegram: prune stale DM topic binding on Thread-not-found (#31501)
Both fallback sites that currently log "Thread X not found, retrying without message_thread_id" now also drop the ``telegram_dm_topic_bindings`` row keyed on ``(chat_id, thread_id)``: * The streaming send loop (``send`` body) — fires on the second failure, after the same-thread one-shot retry confirms the thread really is gone (the first attempt is left alone because Bot API has been observed to return a transient "Thread not found" that recovers on immediate retry). * The control-message helper ``_send_message_with_thread_fallback`` (approval prompts, model picker, update prompts) — single-shot retry, prune unconditionally on the BadRequest match. Without this prune, a user who deletes a Telegram DM topic in the client keeps getting their next inbound message recovered back to the dead thread by ``_recover_telegram_topic_thread_id`` in ``gateway/run.py``, which walks the per-user binding list newest-first and treats the deleted thread as authoritative. The reproduction in the bug report is exactly this: tool progress, approvals, activity messages and replies all land in the wrong place until the user manually runs DELETE on state.db. Cleanup is best-effort — we log at INFO when it succeeds, swallow any exception from the SessionDB call, and the user-facing send proceeds either way. Refs #31501
1 parent 4849a8e commit 142a575

1 file changed

Lines changed: 55 additions & 1 deletion

File tree

plugins/platforms/telegram/adapter.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,47 @@ def _message_thread_id_for_typing(cls, thread_id: Optional[str]) -> Optional[int
810810
def _is_thread_not_found_error(error: Exception) -> bool:
811811
return "thread not found" in str(error).lower()
812812

813+
def _prune_stale_dm_topic_binding(
814+
self, chat_id: Any, thread_id: Any,
815+
) -> None:
816+
"""Drop the stale ``telegram_dm_topic_bindings`` row for a
817+
topic Telegram has confirmed deleted.
818+
819+
Without this prune the recovery logic in
820+
``gateway.run._recover_telegram_topic_thread_id`` keeps
821+
steering future inbound messages to the dead thread (the
822+
bug behind #31501 — tool progress, approvals, replies all
823+
end up in the wrong place even though the user has moved
824+
on to a fresh topic). Best-effort: we never raise from a
825+
send-fallback path — a failed cleanup must not turn into a
826+
failed user-facing send.
827+
"""
828+
if chat_id is None or thread_id is None:
829+
return
830+
store = getattr(self, "_session_store", None)
831+
if store is None:
832+
return
833+
db = getattr(store, "_db", None)
834+
if db is None or not hasattr(db, "delete_telegram_topic_binding"):
835+
return
836+
try:
837+
removed = db.delete_telegram_topic_binding(
838+
chat_id=str(chat_id), thread_id=str(thread_id),
839+
)
840+
except Exception:
841+
logger.debug(
842+
"[%s] delete_telegram_topic_binding failed for "
843+
"chat=%s thread=%s — skipping prune",
844+
self.name, chat_id, thread_id, exc_info=True,
845+
)
846+
return
847+
if removed:
848+
logger.info(
849+
"[%s] Pruned stale Telegram DM topic binding "
850+
"chat=%s thread=%s (Bot API: thread not found)",
851+
self.name, chat_id, thread_id,
852+
)
853+
813854
@staticmethod
814855
def _is_bad_request_error(error: Exception) -> bool:
815856
name = error.__class__.__name__.lower()
@@ -2670,11 +2711,17 @@ async def send(
26702711
continue
26712712
# Second failure: the thread is genuinely gone.
26722713
# Retry without ``message_thread_id`` so the
2673-
# message still reaches the chat.
2714+
# message still reaches the chat, and prune
2715+
# the stale binding so future inbound
2716+
# messages aren't redirected back to it
2717+
# (#31501).
26742718
logger.warning(
26752719
"[%s] Thread %s not found, retrying without message_thread_id",
26762720
self.name, effective_thread_id,
26772721
)
2722+
self._prune_stale_dm_topic_binding(
2723+
chat_id, effective_thread_id,
2724+
)
26782725
used_thread_fallback = True
26792726
effective_thread_id = None
26802727
thread_kwargs = {"message_thread_id": None}
@@ -3355,6 +3402,13 @@ async def _send_message_with_thread_fallback(self, **kwargs):
33553402
self.name,
33563403
message_thread_id,
33573404
)
3405+
# Same prune as the streaming send path — the
3406+
# control-message retry tells us the topic is gone,
3407+
# so the binding row in state.db must go too
3408+
# (#31501).
3409+
self._prune_stale_dm_topic_binding(
3410+
kwargs.get("chat_id"), message_thread_id,
3411+
)
33583412
retry_kwargs = dict(kwargs)
33593413
retry_kwargs.pop("message_thread_id", None)
33603414
return await self._bot.send_message(**retry_kwargs)

0 commit comments

Comments
 (0)