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.
This commit is contained in:
Teknium
2026-08-24 19:48:01 -07:00
parent cbc8d1804d
commit 48f69e51d3
4 changed files with 55 additions and 0 deletions

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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.

View File

@@ -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 秒刷新一次。