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
- 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
Open PRs0
Fixes on record
-
#1783 fix: streaming message length overflow + Telegram error handling
files:
gateway/platforms/telegram.py,gateway/stream_consumer.pyShow 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)
files:
plugins/platforms/telegram/adapter.py,tests/gateway/test_telegram_format.pyShow 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
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.00means 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