fix(honcho): include user_id in agent cache signature to prevent shared-thread peer contamination

PR #27371 introduced a per-user-peer resolver in HonchoSessionManager,
but the resolved runtime identity is frozen into the manager at first-
message init.  When the gateway session_key intentionally omits the
participant ID (the default for threads via thread_sessions_per_user=
False), a cached AIAgent created by user A is reused for user B's
messages, attributing B's writes to A's resolved Honcho peer and
breaking #27371's per-user-peer contract.

Fix by including user_id and user_id_alt in _agent_config_signature so
the cache key distinguishes participants in shared threads.  Each user
in a shared thread now triggers a fresh AIAgent build (trading prompt-
cache warmth for memory-attribution correctness — the right tradeoff
for an external-memory backend where misattribution is unrecoverable).

The default-None case keeps the signature byte-identical to pre-fix
behavior so this change doesn't invalidate in-flight caches on deploy.
This commit is contained in:
erosika 2026-05-21 22:18:06 +00:00 committed by kshitij
parent 00e6830204
commit c03960decd
2 changed files with 92 additions and 0 deletions

View file

@ -1344,3 +1344,75 @@ class TestCachedAgentInactivityReset:
f"Watchdog would see {idle_secs:.0f}s idle, expected ~{STUCK_FOR}s. "
"Inactivity timeout could not fire for a stuck interrupted turn."
)
class TestAgentConfigSignatureUserId:
"""Regression: shared-thread cache must not reuse an agent across users.
PR #27371 introduces a deterministic per-user-peer resolver in
HonchoSessionManager, but Honcho's resolved runtime user identity is
frozen into the manager at first-message init. When the gateway
session_key intentionally omits the participant ID (the default for
threads via thread_sessions_per_user=False), a cached AIAgent created
by user A is reused for user B's messages, attributing B's writes to
A's resolved Honcho peer. The signature must therefore include
user_id and user_id_alt so per-user agents are built in shared
threads, restoring #27371's per-user-peer contract.
Cost: in a multi-user shared thread, each user triggers a fresh
AIAgent build cold prompt cache for that user's first turn. The
correctness gain is judged to outweigh the per-user cache warmup.
"""
def test_signature_changes_with_user_id(self):
from gateway.run import GatewayRunner
runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"}
sig_a = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400"
)
sig_b = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="491827364"
)
assert sig_a != sig_b
def test_signature_stable_with_same_user_id(self):
from gateway.run import GatewayRunner
runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"}
sig_1 = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400"
)
sig_2 = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400"
)
assert sig_1 == sig_2
def test_signature_changes_with_user_id_alt(self):
from gateway.run import GatewayRunner
runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"}
sig_a = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "",
user_id="86701400", user_id_alt="@igor_tg",
)
sig_b = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "",
user_id="86701400", user_id_alt="@erosika_tg",
)
assert sig_a != sig_b
def test_signature_omits_user_id_when_absent(self):
"""Default-None user_id must not change signatures vs unset call.
Pre-#27371-fix callers passed no user_id kwarg. Keeping the
default-None signature byte-identical to the previous behavior
avoids invalidating in-flight caches the moment this lands.
"""
from gateway.run import GatewayRunner
runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"}
sig_implicit = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "",
)
sig_explicit_none = GatewayRunner._agent_config_signature(
"claude-sonnet-4", runtime, ["hermes-telegram"], "",
user_id=None, user_id_alt=None,
)
assert sig_implicit == sig_explicit_none