Telegram streaming 4096-char overflow handling debate

NousResearch/hermes-agent · cluster 1786 · gateway

Data as of

Nothing open on GitHub. Fix merged in #1783. Check it covers your case.

The short answer

0open issues
0open PRs
11items grouped
Known?
Yes. 11 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #1783. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #51736. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #1783 fix: streaming message length overflow + Telegram error handling

    merged to main

    files: gateway/platforms/telegram.py, gateway/stream_consumer.py

    Show the diff
    • 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:
    • 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
  • #51736 fix(telegram): clip mid-stream overflow instead of splitting (#48648)

    merged to main

    files: plugins/platforms/telegram/adapter.py, tests/gateway/test_telegram_format.py

    Show the diff
    • plugins/platforms/telegram/adapter.py
      @@ -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:
      @@ -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.
      @@ -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
      @@ -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()
      @@ -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
      @@ -961,7 +961,7 @@ async def _fake_send(**kwargs):
                   "-100123",
                   "456",
                   "x" * 6000,
      -            finalize=False,
      +            finalize=True,
                   metadata={"thread_id": "17585"},
               )
       
      @@ -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
       # =========================================================================

6 PRs closed without merging

From the close comments: superseded by another PR 3 · done elsewhere 1 · merged via another change 1 · partial fix 1

  • #1893 This PR fixes the Message_too_long error encountered during Telegram … · done elsewhere → #1783
  • #48663 fix(telegram): truncate mid-stream overflow instead of splitting · superseded by another PR → 73a20a6, #51736, #48648, #50408
  • #48718 fix(telegram): truncate streaming overflow instead of splitting (#48648) · superseded by another PR → 73a20a6, #51736, #48648, #50408
  • #50408 fix(telegram): clip mid-stream overflow instead of splitting (#48648) · merged via another change → #51736
  • #51266 fix: defer Telegram streaming overflow chunks · superseded by another PR → 73a20a6, #51736, #48648, #50408
Show 1 more
  • #51974 fix(telegram): fall through to overflow split when rich finalize fails for oversized content (#51961) · partial fix → 0489863, #51961
Closed issues · 3
  • Closed#1786Telegram streaming edit fails with Message_too_long instead of falling back/splitting
  • Closed#48648fix(telegram): Infinite streamed message duplication loop during 4096-char overflow
  • Closed#51961Telegram streaming with finalize=True still leaves messages truncated at 4096 chars
How this cluster was grouped
  • Link evidence (partly structural links): 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 item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#1783, #51736
Closed issues
3
Attempts
6 PRs closed without merging
Fix link checked
24 Jun 2026

For your agent

The same facts, typed.