mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fix(matrix): make outbound message length configurable (#53026)
Raise the Matrix adapter default chunk size from 4,000 to 16,000 characters and allow overrides via config.yaml or MATRIX_MAX_MESSAGE_LENGTH. Fixes #53026
This commit is contained in:
parent
08626c18be
commit
35e0f56fbf
2 changed files with 131 additions and 12 deletions
|
|
@ -41,6 +41,7 @@ Environment variables:
|
|||
MATRIX_RECOVERY_KEY Recovery key for cross-signing verification after device key rotation
|
||||
MATRIX_DM_MENTION_THREADS Create a thread when bot is @mentioned in a DM (default: false)
|
||||
MATRIX_ALLOW_PUBLIC_ROOMS Allow Matrix tools to create public rooms (default: false)
|
||||
MATRIX_MAX_MESSAGE_LENGTH Outbound message chunk size in characters (default: 16000)
|
||||
MATRIX_APPROVAL_REQUIRE_SENDER
|
||||
Require reaction controls to come from the original requester
|
||||
when requester metadata is available (default: true)
|
||||
|
|
@ -352,9 +353,39 @@ class _MatrixChoicePickerPrompt:
|
|||
bot_reaction_events: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
# Matrix message size limit (4000 chars practical, spec has no hard limit
|
||||
# but clients render poorly above this).
|
||||
MAX_MESSAGE_LENGTH = 4000
|
||||
# Matrix message size limit. The spec allows large events (~65 KB), but very
|
||||
# large bodies can render poorly in some clients. The previous 4,000-char
|
||||
# default was overly conservative and split Markdown tables mid-row (#53026).
|
||||
DEFAULT_MAX_MESSAGE_LENGTH = 16000
|
||||
MATRIX_MAX_MESSAGE_LENGTH_CEILING = 65535
|
||||
|
||||
|
||||
def _resolve_max_message_length(config) -> int:
|
||||
"""Resolve outbound chunk size from config, env, or plugin registry."""
|
||||
extra = getattr(config, "extra", {}) or {}
|
||||
raw = extra.get("max_message_length")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATRIX_MAX_MESSAGE_LENGTH")
|
||||
if raw is None:
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
|
||||
entry = platform_registry.get("matrix")
|
||||
if entry and entry.max_message_length:
|
||||
raw = entry.max_message_length
|
||||
except Exception:
|
||||
pass
|
||||
if raw is None:
|
||||
return DEFAULT_MAX_MESSAGE_LENGTH
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return DEFAULT_MAX_MESSAGE_LENGTH
|
||||
return max(500, min(value, MATRIX_MAX_MESSAGE_LENGTH_CEILING))
|
||||
|
||||
|
||||
# Back-compat alias for callers/tests that import the module constant.
|
||||
MAX_MESSAGE_LENGTH = DEFAULT_MAX_MESSAGE_LENGTH
|
||||
|
||||
# Store directory for E2EE keys and sync state.
|
||||
# Uses get_hermes_home() so each profile gets its own Matrix store.
|
||||
|
|
@ -799,21 +830,22 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
"""Gateway adapter for Matrix (any homeserver)."""
|
||||
|
||||
supports_code_blocks = True # Matrix renders fenced code blocks (HTML/markdown)
|
||||
splits_long_messages = True # send() chunks via truncate_message(MAX_MESSAGE_LENGTH)
|
||||
splits_long_messages = True # send() chunks via truncate_message(max_message_length)
|
||||
|
||||
# Matrix clients commonly reserve typed "/" for client-local commands;
|
||||
# the adapter accepts "!command" as the alias that always reaches Hermes
|
||||
# (see _normalize_matrix_bang_command), so instruction text shows "!".
|
||||
typed_command_prefix = "!"
|
||||
|
||||
# Threshold for detecting Matrix client-side message splits.
|
||||
# When a chunk is near the ~4000-char practical limit, a continuation
|
||||
# is almost certain.
|
||||
_SPLIT_THRESHOLD = 3900
|
||||
|
||||
def __init__(self, config: PlatformConfig):
|
||||
super().__init__(config, Platform.MATRIX)
|
||||
|
||||
self.max_message_length = _resolve_max_message_length(config)
|
||||
# Mirror other platform adapters for tests/tooling that read MAX_MESSAGE_LENGTH.
|
||||
self.MAX_MESSAGE_LENGTH = self.max_message_length
|
||||
# When a chunk is near the outbound limit, a continuation is almost certain.
|
||||
self._split_threshold = max(100, self.max_message_length - 100)
|
||||
|
||||
self._homeserver: str = (
|
||||
config.extra.get("homeserver", "") or os.getenv("MATRIX_HOMESERVER", "")
|
||||
).rstrip("/")
|
||||
|
|
@ -1612,7 +1644,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
return SendResult(success=True)
|
||||
|
||||
formatted = self.format_message(content)
|
||||
chunks = self.truncate_message(formatted, MAX_MESSAGE_LENGTH)
|
||||
chunks = self.truncate_message(formatted, self.max_message_length)
|
||||
|
||||
last_event_id = None
|
||||
for i, chunk in enumerate(chunks):
|
||||
|
|
@ -3607,7 +3639,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
try:
|
||||
pending = self._pending_text_batches.get(key)
|
||||
last_len = getattr(pending, "_last_chunk_len", 0) if pending else 0
|
||||
if last_len >= self._SPLIT_THRESHOLD:
|
||||
if last_len >= self._split_threshold:
|
||||
delay = self._text_batch_split_delay_seconds
|
||||
else:
|
||||
delay = self._text_batch_delay_seconds
|
||||
|
|
@ -4690,6 +4722,8 @@ def _apply_yaml_config(yaml_cfg: dict, matrix_cfg: dict) -> dict | None:
|
|||
os.environ["MATRIX_AUTO_THREAD"] = str(matrix_cfg["auto_thread"]).lower()
|
||||
if "dm_mention_threads" in matrix_cfg and not os.getenv("MATRIX_DM_MENTION_THREADS"):
|
||||
os.environ["MATRIX_DM_MENTION_THREADS"] = str(matrix_cfg["dm_mention_threads"]).lower()
|
||||
if "max_message_length" in matrix_cfg and not os.getenv("MATRIX_MAX_MESSAGE_LENGTH"):
|
||||
os.environ["MATRIX_MAX_MESSAGE_LENGTH"] = str(matrix_cfg["max_message_length"])
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -4735,7 +4769,7 @@ def register(ctx) -> None:
|
|||
allow_all_env="MATRIX_ALLOW_ALL_USERS",
|
||||
cron_deliver_env_var="MATRIX_HOME_ROOM",
|
||||
standalone_sender_fn=_standalone_send,
|
||||
max_message_length=4000,
|
||||
max_message_length=DEFAULT_MAX_MESSAGE_LENGTH,
|
||||
emoji="🔐",
|
||||
allow_update_command=True,
|
||||
)
|
||||
|
|
|
|||
85
tests/gateway/test_matrix_message_length.py
Normal file
85
tests/gateway/test_matrix_message_length.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Tests for Matrix outbound message length configuration (#53026)."""
|
||||
import asyncio
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import PlatformConfig
|
||||
|
||||
|
||||
def _make_adapter(**extra):
|
||||
from plugins.platforms.matrix.adapter import MatrixAdapter
|
||||
|
||||
config = PlatformConfig(
|
||||
enabled=True,
|
||||
token="syt_test_token",
|
||||
extra={
|
||||
"homeserver": "https://matrix.example.org",
|
||||
"user_id": "@bot:example.org",
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
return MatrixAdapter(config)
|
||||
|
||||
|
||||
class TestMatrixMaxMessageLength:
|
||||
def test_default_limit_is_16000(self):
|
||||
adapter = _make_adapter()
|
||||
assert adapter.max_message_length == 16000
|
||||
assert adapter._split_threshold == 15900
|
||||
|
||||
def test_extra_override(self):
|
||||
adapter = _make_adapter(max_message_length=12000)
|
||||
assert adapter.max_message_length == 12000
|
||||
assert adapter._split_threshold == 11900
|
||||
|
||||
def test_env_override(self, monkeypatch):
|
||||
monkeypatch.setenv("MATRIX_MAX_MESSAGE_LENGTH", "20000")
|
||||
adapter = _make_adapter()
|
||||
assert adapter.max_message_length == 20000
|
||||
|
||||
def test_extra_beats_env(self, monkeypatch):
|
||||
monkeypatch.setenv("MATRIX_MAX_MESSAGE_LENGTH", "20000")
|
||||
adapter = _make_adapter(max_message_length=10000)
|
||||
assert adapter.max_message_length == 10000
|
||||
|
||||
def test_invalid_values_fall_back_to_default(self, monkeypatch):
|
||||
monkeypatch.setenv("MATRIX_MAX_MESSAGE_LENGTH", "not-a-number")
|
||||
adapter = _make_adapter()
|
||||
assert adapter.max_message_length == 16000
|
||||
|
||||
def test_values_are_clamped(self):
|
||||
adapter = _make_adapter(max_message_length=100)
|
||||
assert adapter.max_message_length == 500
|
||||
adapter = _make_adapter(max_message_length=999999)
|
||||
assert adapter.max_message_length == 65535
|
||||
|
||||
def test_apply_yaml_config_sets_env(self, monkeypatch):
|
||||
from plugins.platforms.matrix.adapter import _apply_yaml_config
|
||||
|
||||
monkeypatch.delenv("MATRIX_MAX_MESSAGE_LENGTH", raising=False)
|
||||
_apply_yaml_config({}, {"max_message_length": 12000})
|
||||
assert os.getenv("MATRIX_MAX_MESSAGE_LENGTH") == "12000"
|
||||
|
||||
def test_register_uses_default_limit(self):
|
||||
from plugins.platforms.matrix.adapter import DEFAULT_MAX_MESSAGE_LENGTH, register
|
||||
|
||||
ctx = MagicMock()
|
||||
register(ctx)
|
||||
kwargs = ctx.register_platform.call_args[1]
|
||||
assert kwargs["max_message_length"] == DEFAULT_MAX_MESSAGE_LENGTH
|
||||
|
||||
def test_send_uses_configured_limit(self):
|
||||
adapter = _make_adapter(max_message_length=5000)
|
||||
adapter._client = MagicMock()
|
||||
adapter._client.send_message_event = AsyncMock(return_value="evt")
|
||||
long_text = "x" * 12000
|
||||
|
||||
async def _run():
|
||||
with patch.object(adapter, "truncate_message", wraps=adapter.truncate_message) as trunc:
|
||||
await adapter.send("!room:example.org", long_text)
|
||||
trunc.assert_called_once()
|
||||
assert trunc.call_args[0][1] == 5000
|
||||
|
||||
asyncio.run(_run())
|
||||
Loading…
Add table
Add a link
Reference in a new issue