# NousResearch/hermes-agent · Telegram streaming 4096-char overflow handling debate

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 11 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 2 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #1783. Verify it addresses your case.
- **Recommended:** read PR #1783 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-51736`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.16`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #1786: Telegram streaming edit fails with Message_too_long instead of falling back/splitting
- #48648: fix(telegram): Infinite streamed message duplication loop during 4096-char overflow
- #51961: Telegram streaming with finalize=True still leaves messages truncated at 4096 chars

## Prior & competing PRs

- **#1783: fix: streaming message length overflow + Telegram error handling**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/telegram.py`, `gateway/stream_consumer.py`
  - fix hunks:
  - `gateway/platforms/telegram.py`

    ```diff
    @@ -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:
    ```
  - `gateway/stream_consumer.py`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```
- **#51736: fix(telegram): clip mid-stream overflow instead of splitting (#48648)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/platforms/telegram/adapter.py`, `tests/gateway/test_telegram_format.py`
  - fix hunks:
  - `plugins/platforms/telegram/adapter.py`

    ```diff
    @@ -2975,11 +2975,18 @@ async def edit_message(
                     return rich_result
     
             # Pre-flight: if content already exceeds the limit, split-and-deliver
    -        # without round-tripping a doomed edit.
    +        # without round-tripping a doomed edit.  During streaming
    +        # (finalize=False) we truncate instead of splitting — splitting creates
    +        # continuation messages whose IDs become the new edit target, and on
    +        # the next token chunk the full accumulated text is re-edited into the
    +        # continuation, triggering another split → infinite duplication loop
    +        # (#48648).  The full content is delivered when finalize=True.
             if utf16_len(content) > self.MAX_MESSAGE_LENGTH:
    -            return await self._edit_overflow_split(
    -                chat_id, message_id, content, finalize=finalize, metadata=metadata,
    -            )
    +            if finalize:
    +                return await self._edit_overflow_split(
    +                    chat_id, message_id, content, finalize=finalize, metadata=metadata,
    +                )
    +            content = self._truncate_stream_overflow_preview(content)
     
             try:
                 if not finalize:
    ```

    ```diff
    @@ -3028,9 +3035,18 @@ async def edit_message(
                         "[%s] edit_message overflow (%d UTF-16 > %d), splitting",
                         self.name, utf16_len(content), self.MAX_MESSAGE_LENGTH,
                     )
    -                return await self._edit_overflow_split(
    -                    chat_id, message_id, content, finalize=finalize, metadata=metadata,
    +                if finalize:
    +                    return await self._edit_overflow_split(
    +                        chat_id, message_id, content, finalize=finalize, metadata=metadata,
    +                    )
    +                # Mid-stream: truncate and retry instead of splitting (#48648).
    +                truncated = self._truncate_stream_overflow_preview(content)
    +                await self._bot.edit_message_text(
    +                    chat_id=int(chat_id),
    +                    message_id=int(message_id),
    +                    text=truncated,
                     )
    +                return SendResult(success=True, message_id=message_id)
                 # Flood control / RetryAfter — short waits are retried inline,
                 # long waits return a failure immediately so streaming can fall back
                 # to a normal final send instead of leaving a truncated partial.
    ```

    ```diff
    @@ -3093,6 +3109,21 @@ async def edit_message(
                 )
                 return SendResult(success=False, error=str(e))
     
    +    def _truncate_stream_overflow_preview(self, content: str) -> str:
    +        """Return a one-message preview for oversized streaming edits.
    +
    +        Streaming edits must keep targeting the original message. Splitting a
    +        mid-stream preview creates continuation messages and moves the active
    +        message id, so the next accumulated-token edit repeats the overflow
    +        cycle (#48648). Final edits still use ``_edit_overflow_split`` to
    +        deliver the complete response.
    +        """
    +        return self.truncate_message(
    +            content,
    +            self.MAX_MESSAGE_LENGTH,
    +            len_fn=utf16_len,
    +        )[0]
    +
         async def _edit_overflow_split(
             self,
             chat_id: str,
    ```
  - `tests/gateway/test_telegram_format.py`

    ```diff
    @@ -908,12 +908,12 @@ async def test_final_edit_uses_markdownv2_with_plain_fallback(self):
     
         @pytest.mark.asyncio
         async def test_message_too_long_splits_into_continuations_not_silent_truncation(self):
    -        """When edit_message_text exceeds Telegram's 4096 UTF-16 limit, the
    -        adapter must split the content across the existing message + new
    -        continuation messages so the user gets the full reply.  Previously
    -        the adapter best-effort truncated the content with '…' and returned
    -        success=True, dropping everything past the truncation boundary
    -        (#19537)."""
    +        """When edit_message_text exceeds Telegram's 4096 UTF-16 limit on
    +        finalize, the adapter must split the content across the existing
    +        message + new continuation messages so the user gets the full reply.
    +        Previously the adapter best-effort truncated the content with '…' and
    +        returned success=True, dropping everything past the truncation
    +        boundary (#19537)."""
             adapter = TelegramAdapter(PlatformConfig(enabled=True, token="[redacted:secret-assignment]"))
             adapter._bot = MagicMock()
             adapter._bot.edit_message_text = AsyncMock()
    ```

    ```diff
    @@ -926,7 +926,7 @@ async def _fake_send(**kwargs):
     
             # 6000-char content well over the 4096 UTF-16 limit.
             oversized = "x" * 6000
    -        result = await adapter.edit_message("123", "456", oversized, finalize=False)
    +        result = await adapter.edit_message("123", "456", oversized, finalize=True)
     
             # Adapter reports success with continuations populated.
             assert result.success is True
    ```

    ```diff
    @@ -961,7 +961,7 @@ async def _fake_send(**kwargs):
                 "-100123",
                 "456",
                 "x" * 6000,
    -            finalize=False,
    +            finalize=True,
                 metadata={"thread_id": "17585"},
             )
     
    ```

    ```diff
    @@ -970,6 +970,60 @@ async def _fake_send(**kwargs):
             assert all(kwargs.get("message_thread_id") == 17585 for kwargs in sent_kwargs)
             assert sent_kwargs[0]["reply_to_message_id"] == 456
     
    +    @pytest.mark.asyncio
    +    async def test_mid_stream_overflow_truncates_instead_of_splitting(self):
    +        """During streaming (finalize=False), oversized content must be
    +        truncated to fit one message rather than split into continuations.
    +        Splitting mid-stream creates new message IDs that the stream consumer
    +        then edits with the full accumulated text, causing an infinite
    +        duplication loop (#48648)."""
    +        adapter = TelegramAdapter(PlatformConfig(enabled=True, token="[redacted:secret-assignment]"))
    +        adapter._bot = MagicMock()
    +        adapter._bot.edit_message_text = AsyncMock()
    +        _next_id = [1000]
    +
    +        async def _fake_send(**kwargs):
    +            _next_id[0] += 1
    +            return SimpleNamespace(message_id=_next_id[0])
    +
    +        adapter._bot.send_message = AsyncMock(side_effect=_fake_send)
    +
    +        oversized = "x" * 6000
    +        result = await adapter.edit_message("123", "456", oversized, finalize=False)
    +
    +        # Must NOT create continuation messages during streaming.
    +        assert result.success is True
    +        assert adapter._bot.send_message.await_count == 0, (
    +            "mid-stream overflow must not send continuation messages"
    +        )
    +        # message_id stays the original — no shift to a new ID.
    +        assert result.message_id == "456"
    +        # The edit must contain truncated content (≤ MAX_MESSAGE_LENGTH).
    +        edited_text = adapter._bot.edit_message_text.call_args.kwargs["text"]
    +        assert len(edited_text) <= adapter.MAX_MESSAGE_LENGTH
    +
    +    @pytest.mark.asyncio
    +    async def test_mid_stream_reactive_overflow_retries_truncated_edit(self):
    +        """If Telegram rejects a streaming edit as too long, retry with a
    +        one-message preview instead of splitting into continuations."""
    +        adapter = TelegramAdapter(PlatformConfig(enabled=True, token="[redacted:secret-assignment]"))
    +        adapter._bot = MagicMock()
    +        adapter._bot.edit_message_text = AsyncMock(
    +            side_effect=[Exception("Bad Request: message is too long"), None]
    +        )
    +        adapter._bot.send_message = AsyncMock()
    +
    +        content = "x" * adapter.MAX_MESSAGE_LENGTH
    +        result = await adapter.edit_message("123", "456", content, finalize=False)
    +
    +        assert result.success is True
    +        assert result.message_id == "456"
    +        adapter._bot.send_message.assert_not_called()
    +        assert adapter._bot.edit_message_text.await_count == 2
    +        retry_text = adapter._bot.edit_message_text.await_args_list[1].kwargs["text"]
    +        assert len(retry_text) <= adapter.MAX_MESSAGE_LENGTH
    +
    +
     # =========================================================================
     # Telegram guest mention gating
     # =========================================================================
    ```
- **#51974: fix(telegram): fall through to overflow split when rich finalize fails for oversized content (#51961)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `04898631cb72dbf84f5c066c3f87fc8c94df53a7`, `#51961`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#1893: This PR fixes the Message_too_long error encountered during Telegram …**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#1783`
- **#48663: fix(telegram): truncate mid-stream overflow instead of splitting**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `73a20a6ad`, `#51736`, `#48648`, `#50408`
- **#48718: fix(telegram): truncate streaming overflow instead of splitting (#48648)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `73a20a6ad`, `#51736`, `#48648`, `#50408`
- **#50408: fix(telegram): clip mid-stream overflow instead of splitting (#48648)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#51736`
- **#51266: fix: defer Telegram streaming overflow chunks**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `73a20a6ad`, `#51736`, `#48648`, `#50408`

## Fix status

- Merged to main in PR #1783 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-24`.
- Subsystem: `gateway`
