mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
fix(telegram): clip mid-stream overflow instead of splitting (#48648)
This commit is contained in:
parent
47fccc0735
commit
73a20a6ad6
2 changed files with 99 additions and 14 deletions
|
|
@ -2975,11 +2975,18 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
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 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
"[%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 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
)
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -908,12 +908,12 @@ class TestEditMessageStreamingSafety:
|
|||
|
||||
@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="fake-token"))
|
||||
adapter._bot = MagicMock()
|
||||
adapter._bot.edit_message_text = AsyncMock()
|
||||
|
|
@ -926,7 +926,7 @@ class TestEditMessageStreamingSafety:
|
|||
|
||||
# 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 @@ class TestEditMessageStreamingSafety:
|
|||
"-100123",
|
||||
"456",
|
||||
"x" * 6000,
|
||||
finalize=False,
|
||||
finalize=True,
|
||||
metadata={"thread_id": "17585"},
|
||||
)
|
||||
|
||||
|
|
@ -970,6 +970,60 @@ class TestEditMessageStreamingSafety:
|
|||
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="fake-token"))
|
||||
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="fake-token"))
|
||||
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
|
||||
# =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue