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