From eebdb4784664b2f3d0b5a3aa4439f57c242181eb Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 17 Mar 2026 10:55:19 -0700 Subject: [PATCH] fix: handle message length overflow in streaming mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stream consumer now splits messages that exceed the platform's MAX_MESSAGE_LENGTH. When accumulated text grows past the safe limit, the current message is finalized and a new message is started for the overflow — same as how normal sends chunk long responses. Split point prefers line boundaries (rfind newline) for clean breaks. Works for all platforms (Telegram 4096, Discord 2000, etc.) by reading the adapter's MAX_MESSAGE_LENGTH at runtime. Also added a safety net in the Telegram adapter: if edit_message_text still hits MESSAGE_TOO_LONG (e.g. markdown formatting expansion), it truncates and returns success so the stream consumer doesn't die. --- gateway/platforms/telegram.py | 14 ++++++++++++++ gateway/stream_consumer.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 5a3c80630098f..fe869f18e1821 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -430,6 +430,20 @@ async def edit_message( # "Message is not modified" — content identical, treat as success if "not modified" in err_str: return SendResult(success=True, message_id=message_id) + # Message too long — content exceeded 4096 chars (e.g. during + # streaming). Truncate and succeed so the stream consumer can + # split the overflow into a new message instead of dying. + if "message_too_long" in err_str or "too long" in err_str: + truncated = content[: self.MAX_MESSAGE_LENGTH - 20] + "…" + try: + await self._bot.edit_message_text( + chat_id=int(chat_id), + message_id=int(message_id), + text=truncated, + ) + except Exception: + pass # best-effort truncation + return SendResult(success=True, message_id=message_id) # Flood control / RetryAfter — back off and retry once retry_after = getattr(e, "retry_after", None) if retry_after is not None or "retry after" in err_str: diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index 1b264c534672c..2ceb0fb1d815e 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -87,6 +87,10 @@ def finish(self) -> None: async def run(self) -> None: """Async task that drains the queue and edits the platform message.""" + # Platform message length limit — leave room for cursor + formatting + _raw_limit = getattr(self.adapter, "MAX_MESSAGE_LENGTH", 4096) + _safe_limit = max(500, _raw_limit - len(self.cfg.cursor) - 100) + try: while True: # Drain all available items from the queue @@ -112,6 +116,21 @@ async def run(self) -> None: ) if should_edit and self._accumulated: + # Split overflow: if accumulated text exceeds the platform + # limit, finalize the current message and start a new one. + while ( + len(self._accumulated) > _safe_limit + and self._message_id is not None + ): + split_at = self._accumulated.rfind("\n", 0, _safe_limit) + if split_at < _safe_limit // 2: + split_at = _safe_limit + chunk = self._accumulated[:split_at] + await self._send_or_edit(chunk) + self._accumulated = self._accumulated[split_at:].lstrip("\n") + self._message_id = None + self._last_sent_text = "" + display_text = self._accumulated if not got_done: display_text += self.cfg.cursor