Skip to content

feat(gateway): per-channel profile routing via channel_routes config - #22262

Closed
andyg5000-agent wants to merge 1 commit into
NousResearch:mainfrom
andyg5000-agent:feat/channel-profile-routing
Closed

feat(gateway): per-channel profile routing via channel_routes config#22262
andyg5000-agent wants to merge 1 commit into
NousResearch:mainfrom
andyg5000-agent:feat/channel-profile-routing

Conversation

@andyg5000-agent

@andyg5000-agent andyg5000-agent commented May 9, 2026

Copy link
Copy Markdown

Problem

A single Hermes gateway instance serves all connected chats/groups/channels with one model, one set of credentials, and one identity. There is no way to route different Signal groups, Telegram chats, or Discord channels to different profiles with their own models, SOUL.md, memories, and API keys.

Users working around this run multiple gateway instances (one per profile), which is fragile and wastes resources.

Solution

Add channel_routes config that maps chat IDs to Hermes profiles. When a message arrives from a routed chat, the gateway resolves the target profile and overrides:

  • Model - from the profile's config.yaml (model.default or model.model)
  • Provider / base_url - from the profile's config
  • API key - from the profile's .env (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)
  • SOUL.md - replaces the global identity/persona
  • Memories - USER.md + MEMORY.md from the profile replace global memories
  • Context files - skipped (AGENTS.md, CLAUDE.md not loaded) to avoid leaking global context

Config Example

channel_routes:
  "group:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=":
    profile: profile_a
  "group:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=":
    profile: profile_b
  "group:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=":
    profile: profile_c
  "+10000000000":
    profile: profile_a

Architecture

New module gateway/channel_routing.py:

  • resolve_channel_route() - matches chat_id to config, loads profile into ProfileContext dataclass
  • build_routed_runtime_kwargs() - extracts api_key/base_url/provider for AIAgent construction
  • build_routed_ephemeral_prompt() - combines SOUL.md + memories + platform context

Gateway integration (gateway/run.py):

  1. Resolve route after session creation in inbound handler (non-fatal fallback)
  2. Thread route_context through _run_agent() call chain (main path + goal continuation path)
  3. Override model/runtime/ephemeral prompt when route matches
  4. Set skip_context_files/skip_memory to prevent double-loading global context

Testing

14 tests covering:

  • Empty/missing/no-match routes return None
  • String and dict route formats
  • SOUL.md and memory loading
  • Custom provider with no-key fallback
  • Runtime kwargs and ephemeral prompt construction
  • Module import smoke tests

Documentation

New website/docs/user-guide/features/channel-routing.md with setup guide, chat ID formats per platform, troubleshooting, and config examples.

What This Does NOT Change

  • Profiles without a route match continue using the default gateway profile (zero breaking change)
  • Route resolution errors are logged at DEBUG level and silently fall back to default
  • Skills still come from the gateway's installed skills (not the routed profile) - this keeps tool schemas consistent across channels

@andyg5000-agent
andyg5000-agent force-pushed the feat/channel-profile-routing branch 2 times, most recently from 40b08d2 to 66396d9 Compare May 9, 2026 04:50
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have labels May 9, 2026
Route different chats, groups, or channels to different Hermes profiles so a
single gateway can serve multiple isolated personas with their own models,
credentials, memories, and identities.

New module gateway/channel_routing.py:
  - resolve_channel_route() — match chat_id to config routes, load profile
    config, .env, SOUL.md, and memories into a ProfileContext dataclass
  - build_routed_runtime_kwargs() — extract api_key/base_url/provider for AIAgent
  - build_routed_ephemeral_prompt() — combine SOUL.md + memories + platform context

Gateway integration (gateway/run.py):
  - Resolve route after session creation in inbound handler
  - Thread route_context through _run_agent() call chain (main + continuation)
  - Override model, runtime credentials, and ephemeral prompt when route matches
  - Set skip_context_files/skip_memory to prevent double-loading global context

Config example in config.yaml:
    channel_routes:
      group:abc123...: {profile: family}
      +123****7890: personal

Includes tests (14 passing) and user-facing documentation.
@andyg5000-agent
andyg5000-agent force-pushed the feat/channel-profile-routing branch from 66396d9 to d8bccdf Compare May 9, 2026 05:19
@andyg5000

Copy link
Copy Markdown

If you're looking for something to manage this without patching hermes, check out https://github.com/agents-blueoi/signal-mux. It allows you to have a single signal-cli running with multiple ports for hermes profiles to subscribe to for routing groups (signal) -> profiles (hermes)

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the concrete routing proposal and tests. The use case remains distinct from current main: channel_overrides only covers model/provider/prompt (gateway/config.py:403-434), while multiplexing requires a separate adapter credential per profile (website/docs/user-guide/multi-profile-gateways.md:158-165).

Problems

  • The PR creates the gateway session before route resolution at gateway/run.py:6331-6344 and never stamps source.profile. That leaves routed messages in the gateway profile’s session namespace, rather than the profile-scoped keying current main uses in gateway/session.py:1257-1322.
  • gateway/channel_routing.py:183-193 manually recognizes only OpenRouter, Anthropic, and OpenAI keys. The routed runtime then bypasses current main’s profile secret scope (gateway/run.py:1413-1444), which is the fail-closed path for profile credentials.
  • The gateway integration has substantially moved: the PR no longer applies cleanly to current gateway/run.py (git apply --check fails on its inbound-handler hunk).

Suggested changes

  • Build chat-route resolution into the current multiplexing boundary before session creation, set SessionSource.profile, and use _profile_runtime_scope() for the whole routed turn.
  • Add end-to-end coverage for session namespace, provider-secret isolation, memory/skills scope, and reply delivery for two channels sharing one adapter.

Automated hermes-sweeper review.

Comment thread gateway/run.py
@@ -6331,6 +6331,20 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
session_entry = self.session_store.get_or_create_session(source)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Route resolution must occur before this session is created, and the resolved profile needs to be stamped on source. Otherwise the SessionStore derives the default gateway namespace and the routed turn cannot own profile-isolated session state.


# Load .env for credentials
dotenv = _load_profile_dotenv(profile_dir)
api_key = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hand-picked API-key list bypasses Hermes’s provider and credential-scope resolution, so routed profiles using OAuth, credential pools, or another supported provider will not receive their configured credentials. Route through the profile-scoped runtime/secret resolver instead.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 13, 2026
@teknium1

Copy link
Copy Markdown
Contributor

The capability this PR implements has now landed on main via PR #64835 (a salvage of #20096 by @Burgunthy, the earliest submission of this feature): gateway.profile_routes routes guilds/channels/threads to profiles at build_source() time, with full per-profile HERMES_HOME isolation (config, skills, memory, secrets, agent:<profile> session namespace) through the multiplex runtime scope.

Your channel_routes approach (per-chat model/credential/SOUL overlay via prompt injection) is covered as a strict subset — the merged feature supplies the profile's model, credentials, SOUL, memories, skills, and sessions through real HERMES_HOME scoping rather than system-prompt overlays, and the matcher additionally supports guild and thread specificity.

Docs: https://hermes-agent.nousresearch.com/docs/user-guide/multi-profile-gateways

Thank you for the work and for pushing on this use case — the demand from PRs like this one is what got the feature prioritized. Closing as superseded by the merged implementation.

@teknium1 teknium1 closed this Jul 15, 2026
@andyg5000

Copy link
Copy Markdown

Thanks @teknium1 !

@teknium1 teknium1 added the area/profiles Multi-profile isolation, HERMES_HOME scoping label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants