fix(honcho): truncate resolve_session_name output to Honcho's 100-char limit (#13868)

Gateway session keys (Matrix "!room:server" + thread event IDs, Telegram
supergroup reply chains, Slack thread IDs with long workspace prefixes) can
exceed Honcho's 100-character session ID limit after sanitization. Every
Honcho API call for those sessions then 400s with "session_id too long".

Add a helper that enforces the 100-char limit after sanitization:
short keys (the common case) short-circuit unchanged; over-limit keys
keep a prefix and append a deterministic `-<8 hex>` SHA-256 suffix over
the original key so two long keys sharing a leading segment can't
collide onto the same truncated ID.

Adds 7 regression tests in tests/honcho_plugin/test_client.py covering
short / exact-limit / long / deterministic / collision-resistant /
allowlist-preserving / hash-suffix-present cases.
This commit is contained in:
Sanjays2402 2026-04-22 01:52:37 -07:00 committed by Erosika
parent 6613054047
commit 4b186d0c53
2 changed files with 112 additions and 1 deletions

View file

@ -534,6 +534,41 @@ class HonchoClientConfig:
pass
return None
# Honcho enforces a 100-char limit on session IDs. Long gateway session keys
# (Matrix "!room:server" + thread event IDs, Telegram supergroup reply
# chains, Slack thread IDs with long workspace prefixes) can overflow this
# limit after sanitization; the Honcho API then rejects every call for that
# session with "session_id too long". See issue #13868.
_HONCHO_SESSION_ID_MAX_LEN = 100
_HONCHO_SESSION_ID_HASH_LEN = 8
@classmethod
def _enforce_session_id_limit(cls, sanitized: str, original: str) -> str:
"""Truncate a sanitized session ID to Honcho's 100-char limit.
The common case (short keys) short-circuits with no modification.
For over-limit keys, keep a prefix of the sanitized ID and append a
deterministic ``-<sha256 prefix>`` suffix so two distinct long keys
that share a leading segment don't collide onto the same truncated ID.
The hash is taken over the *original* pre-sanitization key, so two
inputs that sanitize to the same string still collide intentionally
(same logical session), but two inputs that only share a prefix do not.
"""
max_len = cls._HONCHO_SESSION_ID_MAX_LEN
if len(sanitized) <= max_len:
return sanitized
import hashlib
hash_len = cls._HONCHO_SESSION_ID_HASH_LEN
digest = hashlib.sha256(original.encode("utf-8")).hexdigest()[:hash_len]
# max_len - hash_len - 1 (for the '-' separator) chars of the sanitized
# prefix, then '-<hash>'. Strip any trailing hyphen from the prefix so
# the result doesn't double up on separators.
prefix_len = max_len - hash_len - 1
prefix = sanitized[:prefix_len].rstrip("-")
return f"{prefix}-{digest}"
def resolve_session_name(
self,
cwd: str | None = None,
@ -578,7 +613,7 @@ class HonchoClientConfig:
if gateway_session_key:
sanitized = re.sub(r'[^a-zA-Z0-9_-]+', '-', gateway_session_key).strip('-')
if sanitized:
return sanitized
return self._enforce_session_id_limit(sanitized, gateway_session_key)
# per-session: inherit Hermes session_id (new Honcho session each run)
if self.session_strategy == "per-session" and session_id:

View file

@ -656,6 +656,82 @@ class TestResolveSessionNameGatewayKey:
assert ":" not in result
class TestResolveSessionNameLengthLimit:
"""Regression tests for Honcho's 100-char session ID limit (issue #13868).
Long gateway session keys (Matrix room+event IDs, Telegram supergroup
reply chains, Slack thread IDs with long workspace prefixes) can overflow
Honcho's 100-char session_id limit after sanitization. Before this fix,
every Honcho API call for those sessions 400'd with "session_id too long".
"""
HONCHO_MAX = 100
def test_short_gateway_key_unchanged(self):
"""Short keys must not get a hash suffix appended."""
config = HonchoClientConfig()
result = config.resolve_session_name(
gateway_session_key="agent:main:telegram:dm:8439114563",
)
# Unchanged fast-path: sanitize only, no truncation, no hash suffix.
assert result == "agent-main-telegram-dm-8439114563"
assert len(result) <= self.HONCHO_MAX
def test_key_at_exact_limit_unchanged(self):
"""A sanitized key that is exactly 100 chars must be returned as-is."""
key = "a" * self.HONCHO_MAX
config = HonchoClientConfig()
result = config.resolve_session_name(gateway_session_key=key)
assert result == key
assert len(result) == self.HONCHO_MAX
def test_long_gateway_key_truncated_to_limit(self):
"""An over-limit sanitized key must truncate to exactly 100 chars."""
key = "!roomid:matrix.example.org|" + "$event_" + ("a" * 300)
config = HonchoClientConfig()
result = config.resolve_session_name(gateway_session_key=key)
assert result is not None
assert len(result) == self.HONCHO_MAX
def test_truncation_is_deterministic(self):
"""Same long key must always produce the same truncated session ID."""
key = "matrix-" + ("a" * 300)
config = HonchoClientConfig()
first = config.resolve_session_name(gateway_session_key=key)
second = config.resolve_session_name(gateway_session_key=key)
assert first == second
def test_truncated_result_respects_char_allowlist(self):
"""Truncated result must still match Honcho's [a-zA-Z0-9_-] allowlist."""
import re
key = "slack:T12345:thread-reply:" + ("x" * 300) + ":with:colons:and:slashes/here"
config = HonchoClientConfig()
result = config.resolve_session_name(gateway_session_key=key)
assert result is not None
assert re.fullmatch(r"[a-zA-Z0-9_-]+", result)
def test_distinct_long_keys_do_not_collide(self):
"""Two long keys sharing a prefix must produce different truncated IDs."""
prefix = "matrix:!room:example.org|" + "a" * 200
key_a = prefix + "-suffix-alpha"
key_b = prefix + "-suffix-beta"
config = HonchoClientConfig()
result_a = config.resolve_session_name(gateway_session_key=key_a)
result_b = config.resolve_session_name(gateway_session_key=key_b)
assert result_a != result_b
assert len(result_a) == self.HONCHO_MAX
assert len(result_b) == self.HONCHO_MAX
def test_truncated_result_has_hash_suffix(self):
"""Truncated IDs must end with '-<8 hex chars>' for collision resistance."""
import re
key = "matrix-" + ("a" * 300)
config = HonchoClientConfig()
result = config.resolve_session_name(gateway_session_key=key)
# Last 9 chars: '-' + 8 hex chars.
assert re.search(r"-[0-9a-f]{8}$", result)
class TestResetHonchoClient:
def test_reset_clears_singleton(self):
import plugins.memory.honcho.client as mod