From 4849a8e55583d5eb83c838c7c7be659c19201a3e Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sun, 24 May 2026 21:01:23 +0700 Subject: [PATCH] hermes_state: add SessionDB.delete_telegram_topic_binding (#31501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Targeted ``(chat_id, thread_id)`` prune for the ``telegram_dm_topic_bindings`` table — the missing piece for #31501, where the Telegram adapter detects a topic the user deleted out-of-band but the binding row keeps living in state.db. The recovery logic in ``gateway.run._recover_telegram_topic_thread_id`` then steers every future inbound message back to the dead topic, dropping tool progress, approvals and replies into the wrong place. Returns the number of rows deleted; silently no-ops when the topic-mode tables haven't been migrated yet (read-only / pristine profile) so the helper is safe to call from a send-fallback hot path before the schema has run. --- hermes_state.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/hermes_state.py b/hermes_state.py index c4d07268972..d307db7a735 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -4598,6 +4598,49 @@ class SessionDB: return None return dict(row) if row else None + def delete_telegram_topic_binding( + self, + *, + chat_id: str, + thread_id: str, + ) -> int: + """Remove the binding row for a single (chat, thread) pair. + + Called when the Telegram Bot API confirms a topic was deleted + externally (``Thread not found`` after the same-thread retry + already failed). Without this prune, the stale row keeps + living in ``telegram_dm_topic_bindings`` and the + recovery logic in ``gateway.run._recover_telegram_topic_thread_id`` + cheerfully redirects future inbound messages to the deleted + topic, causing tool progress, approvals, and replies to land + in the wrong place. Issue #31501. + + Returns the number of rows deleted (0 when the binding was + already absent or the topic-mode tables haven't been + migrated yet — both are silent no-ops; we never raise from + a cleanup hot path). + """ + chat_id = str(chat_id) + thread_id = str(thread_id) + deleted = {"count": 0} + + def _do(conn): + try: + cursor = conn.execute( + """ + DELETE FROM telegram_dm_topic_bindings + WHERE chat_id = ? AND thread_id = ? + """, + (chat_id, thread_id), + ) + deleted["count"] = cursor.rowcount or 0 + except sqlite3.OperationalError: + # Tables don't exist yet — nothing to prune. + deleted["count"] = 0 + + self._execute_write(_do) + return deleted["count"] + def bind_telegram_topic( self, *,