{"repo":"NousResearch/hermes-agent","public_id":"1786","display_label":"Telegram streaming 4096-char overflow handling debate","summary":"Multiple competing approaches to handling Telegram's 4096-character message limit during streaming, including truncation, splitting, deferral, and fallback strategies. No single resolution is evident; the cluster reflects ongoing disagreement about the correct fix.","subsystem":"gateway","counts":{"members":11,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":2,"attempt_prs":6},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":1783},"resolution":{"state":"partially_resolved","by":"pr-51736"},"freshness":{"state":"verified","last_verified_at":"2026-06-24T07:00:46+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.15854545454545452},"fix_edges":[{"issue_number":1786,"pr_number":1783,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":48648,"pr_number":51736,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":1786,"title":"Telegram streaming edit fails with Message_too_long instead of falling back/splitting","url":null,"state":"closed"},{"kind":"issue","number":48648,"title":"fix(telegram): Infinite streamed message duplication loop during 4096-char overflow","url":null,"state":"closed"},{"kind":"issue","number":51961,"title":"Telegram streaming with finalize=True still leaves messages truncated at 4096 chars","url":null,"state":"closed"},{"kind":"pr","number":1783,"title":"fix: streaming message length overflow + Telegram error handling","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/telegram.py","gateway/stream_consumer.py"],"fix_hunks":[{"file":"gateway/platforms/telegram.py","hunks":["@@ -430,6 +430,20 @@ async def edit_message(\n             # \"Message is not modified\" — content identical, treat as success\n             if \"not modified\" in err_str:\n                 return SendResult(success=True, message_id=message_id)\n+            # Message too long — content exceeded 4096 chars (e.g. during\n+            # streaming).  Truncate and succeed so the stream consumer can\n+            # split the overflow into a new message instead of dying.\n+            if \"message_too_long\" in err_str or \"too long\" in err_str:\n+                truncated = content[: self.MAX_MESSAGE_LENGTH - 20] + \"…\"\n+                try:\n+                    await self._bot.edit_message_text(\n+                        chat_id=int(chat_id),\n+                        message_id=int(message_id),\n+                        text=truncated,\n+                    )\n+                except Exception:\n+                    pass  # best-effort truncation\n+                return SendResult(success=True, message_id=message_id)\n             # Flood control / RetryAfter — back off and retry once\n             retry_after = getattr(e, \"retry_after\", None)\n             if retry_after is not None or \"retry after\" in err_str:"]},{"file":"gateway/stream_consumer.py","hunks":["@@ -87,6 +87,10 @@ def finish(self) -> None:\n \n     async def run(self) -> None:\n         \"\"\"Async task that drains the queue and edits the platform message.\"\"\"\n+        # Platform message length limit — leave room for cursor + formatting\n+        _raw_limit = getattr(self.adapter, \"MAX_MESSAGE_LENGTH\", 4096)\n+        _safe_limit = max(500, _raw_limit - len(self.cfg.cursor) - 100)\n+\n         try:\n             while True:\n                 # Drain all available items from the queue","@@ -112,6 +116,21 @@ async def run(self) -> None:\n                 )\n \n                 if should_edit and self._accumulated:\n+                    # Split overflow: if accumulated text exceeds the platform\n+                    # limit, finalize the current message and start a new one.\n+                    while (\n+                        len(self._accumulated) > _safe_limit\n+                        and self._message_id is not None\n+                    ):\n+                        split_at = self._accumulated.rfind(\"\\n\", 0, _safe_limit)\n+                        if split_at < _safe_limit // 2:\n+                            split_at = _safe_limit\n+                        chunk = self._accumulated[:split_at]\n+                        await self._send_or_edit(chunk)\n+                        self._accumulated = self._accumulated[split_at:].lstrip(\"\\n\")\n+                        self._message_id = None\n+                        self._last_sent_text = \"\"\n+\n                     display_text = self._accumulated\n                     if not got_done:\n                         display_text += self.cfg.cursor\n"]}]},{"kind":"pr","number":1893,"title":"This PR fixes the Message_too_long error encountered during Telegram …","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1783","number":1783}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":48663,"title":"fix(telegram): truncate mid-stream overflow instead of splitting","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"73a20a6ad","number":0},{"kind":"pr","value":"#51736","number":51736},{"kind":"pr","value":"#48648","number":48648},{"kind":"pr","value":"#50408","number":50408}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":48718,"title":"fix(telegram): truncate streaming overflow instead of splitting (#48648)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"73a20a6ad","number":0},{"kind":"pr","value":"#51736","number":51736},{"kind":"pr","value":"#48648","number":48648},{"kind":"pr","value":"#50408","number":50408}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":50408,"title":"fix(telegram): clip mid-stream overflow instead of splitting (#48648)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#51736","number":51736}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":51266,"title":"fix: defer Telegram streaming overflow chunks","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"73a20a6ad","number":0},{"kind":"pr","value":"#51736","number":51736},{"kind":"pr","value":"#48648","number":48648},{"kind":"pr","value":"#50408","number":50408}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":51736,"title":"fix(telegram): clip mid-stream overflow instead of splitting (#48648)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","tests/gateway/test_telegram_format.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -2975,11 +2975,18 @@ async def edit_message(\n                 return rich_result\n \n         # Pre-flight: if content already exceeds the limit, split-and-deliver\n-        # without round-tripping a doomed edit.\n+        # without round-tripping a doomed edit.  During streaming\n+        # (finalize=False) we truncate instead of splitting — splitting creates\n+        # continuation messages whose IDs become the new edit target, and on\n+        # the next token chunk the full accumulated text is re-edited into the\n+        # continuation, triggering another split → infinite duplication loop\n+        # (#48648).  The full content is delivered when finalize=True.\n         if utf16_len(content) > self.MAX_MESSAGE_LENGTH:\n-            return await self._edit_overflow_split(\n-                chat_id, message_id, content, finalize=finalize, metadata=metadata,\n-            )\n+            if finalize:\n+                return await self._edit_overflow_split(\n+                    chat_id, message_id, content, finalize=finalize, metadata=metadata,\n+                )\n+            content = self._truncate_stream_overflow_preview(content)\n \n         try:\n             if not finalize:","@@ -3028,9 +3035,18 @@ async def edit_message(\n                     \"[%s] edit_message overflow (%d UTF-16 > %d), splitting\",\n                     self.name, utf16_len(content), self.MAX_MESSAGE_LENGTH,\n                 )\n-                return await self._edit_overflow_split(\n-                    chat_id, message_id, content, finalize=finalize, metadata=metadata,\n+                if finalize:\n+                    return await self._edit_overflow_split(\n+                        chat_id, message_id, content, finalize=finalize, metadata=metadata,\n+                    )\n+                # Mid-stream: truncate and retry instead of splitting (#48648).\n+                truncated = self._truncate_stream_overflow_preview(content)\n+                await self._bot.edit_message_text(\n+                    chat_id=int(chat_id),\n+                    message_id=int(message_id),\n+                    text=truncated,\n                 )\n+                return SendResult(success=True, message_id=message_id)\n             # Flood control / RetryAfter — short waits are retried inline,\n             # long waits return a failure immediately so streaming can fall back\n             # to a normal final send instead of leaving a truncated partial.","@@ -3093,6 +3109,21 @@ async def edit_message(\n             )\n             return SendResult(success=False, error=str(e))\n \n+    def _truncate_stream_overflow_preview(self, content: str) -> str:\n+        \"\"\"Return a one-message preview for oversized streaming edits.\n+\n+        Streaming edits must keep targeting the original message. Splitting a\n+        mid-stream preview creates continuation messages and moves the active\n+        message id, so the next accumulated-token edit repeats the overflow\n+        cycle (#48648). Final edits still use ``_edit_overflow_split`` to\n+        deliver the complete response.\n+        \"\"\"\n+        return self.truncate_message(\n+            content,\n+            self.MAX_MESSAGE_LENGTH,\n+            len_fn=utf16_len,\n+        )[0]\n+\n     async def _edit_overflow_split(\n         self,\n         chat_id: str,"]},{"file":"tests/gateway/test_telegram_format.py","hunks":["@@ -908,12 +908,12 @@ async def test_final_edit_uses_markdownv2_with_plain_fallback(self):\n \n     @pytest.mark.asyncio\n     async def test_message_too_long_splits_into_continuations_not_silent_truncation(self):\n-        \"\"\"When edit_message_text exceeds Telegram's 4096 UTF-16 limit, the\n-        adapter must split the content across the existing message + new\n-        continuation messages so the user gets the full reply.  Previously\n-        the adapter best-effort truncated the content with '…' and returned\n-        success=True, dropping everything past the truncation boundary\n-        (#19537).\"\"\"\n+        \"\"\"When edit_message_text exceeds Telegram's 4096 UTF-16 limit on\n+        finalize, the adapter must split the content across the existing\n+        message + new continuation messages so the user gets the full reply.\n+        Previously the adapter best-effort truncated the content with '…' and\n+        returned success=True, dropping everything past the truncation\n+        boundary (#19537).\"\"\"\n         adapter = TelegramAdapter(PlatformConfig(enabled=True, token=\"[redacted:secret-assignment]\"))\n         adapter._bot = MagicMock()\n         adapter._bot.edit_message_text = AsyncMock()","@@ -926,7 +926,7 @@ async def _fake_send(**kwargs):\n \n         # 6000-char content well over the 4096 UTF-16 limit.\n         oversized = \"x\" * 6000\n-        result = await adapter.edit_message(\"123\", \"456\", oversized, finalize=False)\n+        result = await adapter.edit_message(\"123\", \"456\", oversized, finalize=True)\n \n         # Adapter reports success with continuations populated.\n         assert result.success is True","@@ -961,7 +961,7 @@ async def _fake_send(**kwargs):\n             \"-100123\",\n             \"456\",\n             \"x\" * 6000,\n-            finalize=False,\n+            finalize=True,\n             metadata={\"thread_id\": \"17585\"},\n         )\n ","@@ -970,6 +970,60 @@ async def _fake_send(**kwargs):\n         assert all(kwargs.get(\"message_thread_id\") == 17585 for kwargs in sent_kwargs)\n         assert sent_kwargs[0][\"reply_to_message_id\"] == 456\n \n+    @pytest.mark.asyncio\n+    async def test_mid_stream_overflow_truncates_instead_of_splitting(self):\n+        \"\"\"During streaming (finalize=False), oversized content must be\n+        truncated to fit one message rather than split into continuations.\n+        Splitting mid-stream creates new message IDs that the stream consumer\n+        then edits with the full accumulated text, causing an infinite\n+        duplication loop (#48648).\"\"\"\n+        adapter = TelegramAdapter(PlatformConfig(enabled=True, token=\"[redacted:secret-assignment]\"))\n+        adapter._bot = MagicMock()\n+        adapter._bot.edit_message_text = AsyncMock()\n+        _next_id = [1000]\n+\n+        async def _fake_send(**kwargs):\n+            _next_id[0] += 1\n+            return SimpleNamespace(message_id=_next_id[0])\n+\n+        adapter._bot.send_message = AsyncMock(side_effect=_fake_send)\n+\n+        oversized = \"x\" * 6000\n+        result = await adapter.edit_message(\"123\", \"456\", oversized, finalize=False)\n+\n+        # Must NOT create continuation messages during streaming.\n+        assert result.success is True\n+        assert adapter._bot.send_message.await_count == 0, (\n+            \"mid-stream overflow must not send continuation messages\"\n+        )\n+        # message_id stays the original — no shift to a new ID.\n+        assert result.message_id == \"456\"\n+        # The edit must contain truncated content (≤ MAX_MESSAGE_LENGTH).\n+        edited_text = adapter._bot.edit_message_text.call_args.kwargs[\"text\"]\n+        assert len(edited_text) <= adapter.MAX_MESSAGE_LENGTH\n+\n+    @pytest.mark.asyncio\n+    async def test_mid_stream_reactive_overflow_retries_truncated_edit(self):\n+        \"\"\"If Telegram rejects a streaming edit as too long, retry with a\n+        one-message preview instead of splitting into continuations.\"\"\"\n+        adapter = TelegramAdapter(PlatformConfig(enabled=True, token=\"[redacted:secret-assignment]\"))\n+        adapter._bot = MagicMock()\n+        adapter._bot.edit_message_text = AsyncMock(\n+            side_effect=[Exception(\"Bad Request: message is too long\"), None]\n+        )\n+        adapter._bot.send_message = AsyncMock()\n+\n+        content = \"x\" * adapter.MAX_MESSAGE_LENGTH\n+        result = await adapter.edit_message(\"123\", \"456\", content, finalize=False)\n+\n+        assert result.success is True\n+        assert result.message_id == \"456\"\n+        adapter._bot.send_message.assert_not_called()\n+        assert adapter._bot.edit_message_text.await_count == 2\n+        retry_text = adapter._bot.edit_message_text.await_args_list[1].kwargs[\"text\"]\n+        assert len(retry_text) <= adapter.MAX_MESSAGE_LENGTH\n+\n+\n # =========================================================================\n # Telegram guest mention gating\n # =========================================================================\n"]}]},{"kind":"pr","number":51974,"title":"fix(telegram): fall through to overflow split when rich finalize fails for oversized content (#51961)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"04898631cb72dbf84f5c066c3f87fc8c94df53a7","number":0},{"kind":"pr","value":"#51961","number":51961}],"category":"partial_fix","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true}],"trust":{"default_tier":"verified","includes_suggested":false}}