From 54cf7f4b0f0977a2a1b7c41ece4dcb8b9a23910c Mon Sep 17 00:00:00 2001 From: MacroAnarchy Date: Thu, 26 Mar 2026 23:31:27 +0100 Subject: [PATCH] fix(telegram): complete 3-layer defense against invalid thread_id in non-forum DMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer 1 — Source: Drop spurious message_thread_id in _build_message_event() when no forum topic was resolved. Telegram sets this field on reply chains in non-forum DMs, but it cannot be used as message_thread_id when sending. Layer 2 — Send fallback: When send() gets 'Message thread not found', clear effective_thread_id and retry immediately instead of retrying 3x with the same broken value. Catches both Exception and NetworkError paths. Layer 3 — Progress messages: Make the event_message_id fallback in _progress_thread_id platform-aware. Only Slack/Discord use message IDs as thread identifiers. Telegram uses dedicated forum topic IDs — falling back to a regular message ID causes 'Message thread not found' on every progress update. Without Layer 3, progress/status messages in run.py re-introduce invalid thread IDs even when Layers 1-2 correctly handle the send path. Fixes #3206 --- gateway/platforms/telegram.py | 25 +++++++++++++++++++++++-- gateway/run.py | 9 ++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index bff21982a208..8921ae92724c 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -667,6 +667,8 @@ async def send( except ImportError: _NetErr = OSError # type: ignore[misc,assignment] + effective_thread_id = int(thread_id) if thread_id else None + for i, chunk in enumerate(chunks): should_thread = self._should_thread_reply(reply_to, i) reply_to_id = int(reply_to) if should_thread else None @@ -681,9 +683,16 @@ async def send( text=chunk, parse_mode=ParseMode.MARKDOWN_V2, reply_to_message_id=reply_to_id, - message_thread_id=int(thread_id) if thread_id else None, + message_thread_id=effective_thread_id, ) except Exception as md_error: + # Thread not found — drop thread_id and retry immediately + if "thread not found" in str(md_error).lower(): + if effective_thread_id is not None: + logger.warning("[%s] Thread %s not found, retrying without thread_id", self.name, effective_thread_id) + effective_thread_id = None + continue + raise # Markdown parsing failed, try plain text if "parse" in str(md_error).lower() or "markdown" in str(md_error).lower(): logger.warning("[%s] MarkdownV2 parse failed, falling back to plain text: %s", self.name, md_error) @@ -693,12 +702,18 @@ async def send( text=plain_chunk, parse_mode=None, reply_to_message_id=reply_to_id, - message_thread_id=int(thread_id) if thread_id else None, + message_thread_id=effective_thread_id, ) else: raise break # success except _NetErr as send_err: + # Thread not found may also surface as a network-layer error + if "thread not found" in str(send_err).lower(): + if effective_thread_id is not None: + logger.warning("[%s] Thread %s not found (network layer), retrying without thread_id", self.name, effective_thread_id) + effective_thread_id = None + continue if _send_attempt < 2: wait = 2 ** _send_attempt logger.warning("[%s] Network error on send (attempt %d/3), retrying in %ds: %s", @@ -1811,6 +1826,12 @@ def _build_message_event(self, message: Message, msg_type: MessageType) -> Messa if not chat_topic: chat_topic = created_name + # If no DM topic was resolved, Telegram may have set message_thread_id + # to indicate a reply chain (not a real forum topic). Drop it so + # downstream send calls don't fail with "Message thread not found". + if not chat_topic: + thread_id_str = None + # Build source source = self.build_source( chat_id=str(chat.id), diff --git a/gateway/run.py b/gateway/run.py index fd0d60042baa..d90222b335ed 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -4865,7 +4865,14 @@ def progress_callback(tool_name: str, preview: str = None, args: dict = None): # final reply will be threaded under the original message via reply_to. # Use event_message_id as fallback so progress messages land in the # same thread as the final response instead of going to the DM root. - _progress_thread_id = source.thread_id or event_message_id + # NOTE: Only use event_message_id fallback for platforms that support + # threading by message ID (Slack, Discord). Telegram uses dedicated + # forum topic IDs for message_thread_id — passing a regular message ID + # causes "Message thread not found" errors. + if source.platform in ("slack", "discord"): + _progress_thread_id = source.thread_id or event_message_id + else: + _progress_thread_id = source.thread_id _progress_metadata = {"thread_id": _progress_thread_id} if _progress_thread_id else None async def send_progress_messages():