Follow-ups on the salvaged #20096 profile-routing feature:
- _profile_name_for_source now returns None unless gateway.multiplex_profiles
is on. Routing stamps source.profile, which namespaces session/batch keys,
but the profile-scoped agent run only activates under multiplexing — without
the gate, configured routes with multiplexing off split batch/session keys
into agent:<profile> while the agent still ran from agent:main.
- Widen the profile-aware _text_batch_key fix from Discord to every adapter
that builds batch keys via build_session_key (telegram, whatsapp, matrix,
feishu, wecom, weixin) — routing is platform-generic, so the batch-key
namespace fix must be too.
- Downgrade the no-route-matched log from INFO to DEBUG (fired on every
unrouted inbound message).
- GatewayConfig.to_dict(): serialize profile_routes as plain dicts
(ProfileRoute dataclasses are not JSON-safe).
- Docs: correct the 'independent of multiplexing' claim in
docs/profile-routing.md (routing requires multiplexing), fix the
platform-only specificity row (0, not 1), and document profile_routes in
website/docs/user-guide/multi-profile-gateways.md.
- Tests: pin the multiplex gate (routes ignored when off, active when on,
build_source end-to-end stays in agent:main when off).
Completes the review's ask for "adapter-to-session-key integration coverage
for Discord and a non-Discord platform" on #20096.
Drives a concrete adapter's real BasePlatformAdapter.build_source with an
injected gateway_runner, asserts the matched route's profile is stamped on
the source, and that build_session_key scopes the key under agent:<profile>:
(versus the shared agent:main: namespace). Covers Discord and Telegram — the
Telegram case is the bug-#2 path that previously fell through to default.
Adds a regression anchor: without gateway_runner, profile stays None and the
key lands in agent:main (the silent fallback the fix removes for non-Discord).
Co-Authored-By: Claude <noreply@anthropic.com>
Addresses hermes-sweeper review on #20096.
Problem 1 (profile_routing.py): route matching returned True on a chat_id
hit before the guild_id constraint was consulted, so a route declaring both
guild_id and chat_id matched on chat_id alone. Restored conjunctive (AND)
semantics — every declared discriminator must hold; hierarchical parent_chat_id
matching is preserved. Added a regression test for the guild+chat case.
Problem 2 (base.py / run.py): gateway_runner was injected only when an adapter
pre-declared the attribute, and only Discord did — so build_source never called
_profile_name_for_source for Telegram/Feishu/Slack/etc., despite the
platform-generic claim. Declared gateway_runner on BasePlatformAdapter and made
the plugin-registry injection unconditional, so profile routing now reaches
every platform. Added non-Discord (Telegram) resolution coverage and an
injection-inheritance test.
Also adds docs/profile-routing.md documenting gateway.profile_routes
(matching rules, specificity, profile isolation) — requested in review.
Co-Authored-By: Claude <noreply@anthropic.com>
Three follow-ups to the initial routing PR after code review:
1. Remove dead forum-post hierarchy cache. The `_forum_post_cache`,
`register_forum_post()`, and `resolve_forum_channel()` were never
wired up — no caller in the codebase. Discord's adapter already
sets `parent_chat_id` to the immediate parent (forum channel for a
forum post), so the existing `self.chat_id == parent_chat_id`
branch in `matches()` handles forum posts correctly without a
cache. The hierarchical-resolution branch in `matches()` and the
bounded-LRU infrastructure are removed.
2. Fix docstring specificity numbers (8 → 14, 4 → 6) and rewrite the
"Hierarchical matching" section to describe the actual one-level
parent_chat_id behavior. Removed unused `OrderedDict` and `Set`
imports.
3. Warn loudly when a routed profile doesn't exist on disk. Previously,
a typo in `profile_routes` (e.g. `crypto-tradr`) silently fell back
to the global HERMES_HOME, causing the message to read the default
profile's memory/credentials with no signal to the operator. Now
emits a `logger.warning` with the profile name, source identifier,
and the fallback reason. Bare-exception path also gets `exc_info`.
Tests:
- test_profile_routing.py: +2 tests verifying forum post matching via
direct parent_chat_id (covers the case the removed cache was meant
for). 31 total, all pass.
- test_profile_resolution.py: NEW, 12 tests covering resolution order
(source.profile > routing > active > default), missing-profile
warning, exception handling, and routing consultation. All pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>