From 48f69e51d373f45bbed6f00457807528c5946c0c Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:48:01 -0700 Subject: [PATCH] fix(signal): chunk long standalone sends and cover both delivery paths (salvage #57929 + #67279) Follow-up to lkz-de's adapter chunking commit: long Signal messages no longer truncate on ANY delivery path. - tools/send_message_tool.py: register Signal's 8000-char limit in _MAX_LENGTHS (imported from the adapter module so the two paths can't drift) so hermes send / cron standalone / MCP sends split via the shared truncate_message() pass instead of signal-cli rejecting them. Standalone-path idea credited to @5L-hermes01 (#67279). - tests: regression test proving standalone Signal sends chunk at the adapter limit with no truncation footer (fails on pre-fix main). - docs: Long Messages section on the Signal page (en + zh-Hans). Both fixes verified by sabotage A/B (tests fail with the respective half reverted to origin/main) and a real-import E2E: 27k-char message with emoji + cross-boundary bold + code blocks -> 4 chunks, all styles in-range UTF-16, lossless reassembly. --- tests/tools/test_send_message_tool.py | 35 +++++++++++++++++++ tools/send_message_tool.py | 12 +++++++ website/docs/user-guide/messaging/signal.md | 4 +++ .../current/user-guide/messaging/signal.md | 4 +++ 4 files changed, 55 insertions(+) diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index 93db63da49..5aaffdd590 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -490,6 +490,41 @@ class TestSendToPlatformChunking: for call in send.await_args_list: assert len(call.args[2]) <= 2020 # each chunk fits the limit + def test_signal_long_message_is_chunked(self, monkeypatch): + """Standalone Signal sends split at the adapter's 8000-char limit. + + The standalone path (hermes send / cron / MCP) speaks raw JSON-RPC via + _send_signal and bypasses SignalAdapter.send(), so the shared + truncate_message() pass in _send_to_platform must know Signal's limit + (regression for #67279 / #57929 — long sends were rejected whole). + """ + from gateway.platforms.signal import MAX_MESSAGE_LENGTH as SIGNAL_MAX + import tools.send_message_tool as smt + + sent = [] + + async def fake_send_signal(extra, chat_id, chunk, media_files=None): + sent.append(chunk) + return {"success": True, "platform": "signal", "chat_id": chat_id} + + monkeypatch.setattr(smt, "_send_signal", fake_send_signal) + + long_msg = "word " * ((SIGNAL_MAX // 5) + 500) # comfortably over limit + result = asyncio.run( + _send_to_platform( + Platform.SIGNAL, + SimpleNamespace(enabled=True, token=None, + extra={"http_url": "http://localhost:8080", + "account": "+15551234567"}), + "+15557654321", long_msg, + ) + ) + assert result["success"] is True + assert len(sent) >= 2, "long Signal message must be split, not sent whole" + assert all(len(chunk) <= SIGNAL_MAX for chunk in sent) + # No truncation footer — content is delivered in full across chunks + assert all("truncated, full output saved to" not in c for c in sent) + def test_slack_pre_escaped_entities_not_double_escaped(self, monkeypatch): """Pre-escaped HTML entities survive tool-layer formatting without double-escaping.""" diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index 403d49fdcc..08f72d9509 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -968,6 +968,18 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, Platform.TELEGRAM: TelegramAdapter.MAX_MESSAGE_LENGTH if _telegram_available else 4096, } + # Signal's standalone path (_send_signal) speaks raw JSON-RPC and does not + # go through SignalAdapter.send(), so it never benefits from the adapter's + # native chunking. Register the platform limit here so the shared + # truncate_message() pass below splits long sends instead of signal-cli + # rejecting them. Sourced from the adapter module so the two paths can't + # drift (credit: @5L-hermes01 in #67279, @lkz-de in #57929). + try: + from gateway.platforms.signal import MAX_MESSAGE_LENGTH as _SIGNAL_MAX + _MAX_LENGTHS[Platform.SIGNAL] = _SIGNAL_MAX + except ImportError: + _MAX_LENGTHS[Platform.SIGNAL] = 8000 + # Check plugin registry for max_message_length if platform not in _MAX_LENGTHS: try: diff --git a/website/docs/user-guide/messaging/signal.md b/website/docs/user-guide/messaging/signal.md index 597a7fa30b..a435ed96b6 100644 --- a/website/docs/user-guide/messaging/signal.md +++ b/website/docs/user-guide/messaging/signal.md @@ -181,6 +181,10 @@ Signal messages render with **native formatting** instead of literal markdown ch None of this requires additional config — it ships on by default in recent signal-cli builds. If your `signal-cli` version is too old, Hermes falls back to plaintext delivery and logs a one-time warning. +### Long Messages + +Signal caps a single message at **8,000 characters**. Hermes splits longer responses into numbered chunks (`(1/3)`, `(2/3)`, …) automatically instead of truncating them. This applies to every delivery path — live conversation replies, cron job deliveries, `hermes send`, and MCP `send_message` calls — and native formatting (bold, italic, code, spoilers) is preserved across chunk boundaries. + ### Typing Indicators The bot sends typing indicators while processing messages, refreshing every 8 seconds. diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/signal.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/signal.md index 90e8edcc96..1f73d61a55 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/signal.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/signal.md @@ -182,6 +182,10 @@ Signal 消息以**原生格式**渲染,而非显示原始 markdown 字符。 以上功能无需额外配置——在近期的 signal-cli 版本中默认启用。若你的 `signal-cli` 版本过旧,Hermes 会回退到纯文本投递,并记录一次性警告日志。 +### 长消息 + +Signal 单条消息上限为 **8,000 字符**。Hermes 会自动将更长的回复拆分为带编号的分段(`(1/3)`、`(2/3)`……)发送,而不是截断内容。此行为覆盖所有投递路径——实时对话回复、cron 任务投递、`hermes send` 以及 MCP `send_message` 调用——且原生格式(加粗、斜体、代码、剧透)在分段边界处保持不变。 + ### 正在输入指示器 机器人在处理消息时会发送正在输入指示器,每 8 秒刷新一次。