Skip to content

fix(gateway): resolve per-profile home channel in multiplex mode - #60743

Closed
lexiismadd wants to merge 1 commit into
NousResearch:mainfrom
lexiismadd:fix/per-profile-home-channel-multiplex
Closed

fix(gateway): resolve per-profile home channel in multiplex mode#60743
lexiismadd wants to merge 1 commit into
NousResearch:mainfrom
lexiismadd:fix/per-profile-home-channel-multiplex

Conversation

@lexiismadd

Copy link
Copy Markdown

Summary

When running multiple profiles in a single gateway process (multiplex_profiles: true), each profile needs its own TELEGRAM_HOME_CHANNEL (and equivalent per-platform home channels) so cron deliveries, restart notifications, and the per-chat home-channel notice are scoped to the correct chat.

Previously:

  • get_home_channel() (gateway/config.py) returned a HomeChannel cached once at config-load time via os.getenv(), which always read the default profile's environment.
  • The /sethome notice check in run.py used os.getenv(env_key) directly, bypassing any per-profile resolution.
  • The /sethome handler in slash_commands.py wrote the in-memory cache via platform_config.home_channel = HomeChannel(...), which raced between profiles in multiplex mode (last-write-wins).

The fix:

  • get_home_channel() now reads live via agent.secret_scope.get_secret() when a profile secret scope is active, falling back to os.getenv() in single-profile mode, then to the cached attribute. This honors the active per-profile scope on every call, so cron deliveries, restart notifications, and the per-chat notice all see the correct value.
  • The /sethome notice check in run.py also uses get_secret() for the same reason.
  • The /sethome handler no longer mutates the in-memory cache; the save_env_value() call already persists to the active profile's .env, and the next live read picks up the new value. The in-memory write was racy and is no longer needed.

The existing _HOME_CHANNEL_NAME_ENV_KEYS and thread-id maps remain limited to Telegram/Discord, matching the current scope of name/thread_id resolution. Other platforms fall through to the cached attribute (same as before).

Verified live: lexi DM bot no longer prompts for /sethome after gateway restart despite the lexi .env having TELEGRAM_HOME_CHANNEL=5397107712 (its user ID). The family group's home channel still routes to -1002369818110 correctly.

Files changed

  • gateway/config.py: get_home_channel() now does live resolution via get_secret(); added _HOME_CHANNEL_ENV_KEYS map.
  • gateway/run.py: /sethome notice check uses get_secret() instead of os.getenv().
  • gateway/slash_commands.py: /sethome handler drops in-memory platform_config.home_channel write; save_env_value() handles persistence.

Test plan

Three unit tests added (in PR description; reproducible via venv/bin/python):

  1. Env var set, no scopeget_home_channel() returns env value with correct name/thread_id.
  2. Secret scope active (multiplex)get_home_channel() returns scope value, overriding env.
  3. Empty scope + no env — falls back to cached platform_config.home_channel.

Manual verification on the user's deployment: lexi DM bot stopped prompting for /sethome after gateway restart, and the family group home channel still routes correctly.

Related

  • A pre-existing auto-rename regression (Telegram DM topic not auto-renaming after first reply) was observed during testing but is not addressed by this PR. It is gated on _is_telegram_topic_laneself._session_db.is_telegram_topic_mode_enabled(), and appears to be a separate ContextVar propagation issue in the multiplex refactor that landed recently. Filing as a follow-up.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 8, 2026

@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 tracing the stale process-environment reads; the underlying multiplexing defect is present on current main (gateway/config.py:768-773, gateway/run.py:11399-11402). The implementation needs changes before it can safely provide the claimed guarantee.

Problems

  • The new onboarding get_secret() call is not inside _profile_runtime_scope: direct /sethome dispatch occurs at gateway/run.py:9857, while the normal inbound scope is only entered by _run_agent at gateway/run.py:16846. In multiplex mode an unscoped read raises (agent/secret_scope.py:149-157), and the patch catches it and falls back to process-global os.getenv().
  • The new map uses MATRIX_HOME_CHANNEL and EMAIL_HOME_CHANNEL, but main uses MATRIX_HOME_ROOM (gateway/config.py:1679) and EMAIL_HOME_ADDRESS (gateway/config.py:1713).
  • Removing the in-memory /sethome update does not refresh an active secret-scope mapping; save_env_value() updates os.environ at hermes_cli/config.py:7602, while a scope remains authoritative (agent/secret_scope.py:144-147). Existing same-process behavior is covered at tests/gateway/test_restart_notification.py:182-243.

Suggested changes

  • Scope direct command/onboarding paths by source.profile, use canonical target keys, and add multiplex regressions for secondary-profile /sethome plus Matrix/email targets.

Automated hermes-sweeper review.

Comment thread gateway/run.py
# get_secret so the notice honors the same per-profile resolution
# path as the rest of the gateway.
try:
from agent.secret_scope import get_secret as _get_secret

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 lookup is still outside _profile_runtime_scope: that scope is entered later by _run_agent (gateway/run.py:16846), while this onboarding branch runs during _handle_message_with_agent. In multiplex mode get_secret() therefore raises, and this except falls back to the same cross-profile os.getenv() value the change is intended to eliminate. Scope this path by source.profile instead of falling back to process-global environment state.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 10, 2026
When running multiplex_profiles: true, all profiles shared a single
state.db for session storage. This adds per-profile state.db files so
each profile's sessions, routing tables, and slash commands write to
the correct profile's database.

Changes:
- gateway/run.py: _build_profile_session_dbs() creates per-profile
  AsyncSessionDBs; _session_db_for(source) routes to the correct DB;
  SessionStore wired with get_profile_db callback
- gateway/session.py: SessionStore accepts get_profile_db callback;
  routing table persistence and session reset use per-profile DB
- gateway/slash_commands.py: all slash commands route through
  _session_db_for(source) instead of self._session_db
- gateway/config.py: get_home_channel() reads live via get_secret()
  when a profile secret scope is active (from PR NousResearch#60743)
- hermes_state.py: ON CONFLICT preserves user_id and source via
  COALESCE to prevent null-overwrite on profile-scoped writes
- plugins/platforms/matrix/adapter.py: per-sender/room profile routing
  via MATRIX_SENDER_PROFILE_MAP env var or config.extra maps

Also addresses teknium1 review on PR NousResearch#60743:
- /sethome notice check uses get_secret() instead of os.getenv()
- /sethome handler drops racy in-memory home_channel write
- Matrix routing config documented in adapter docstring
@teknium1

Copy link
Copy Markdown
Contributor

Closing after a hunk-by-hunk review against current main — the home-channel core is superseded and the rest needs a fresh start:

  • The /sethome notice scoping collides with the stronger chain merged via fix(gateway): multiplex credential isolation — authz, Slack, WeChat, secondary adapters (4-PR cluster salvage) #65629 (get_secret → os.getenv → config.get_home_channel → per-profile config re-read).
  • The get_home_channel() live-read is solved differently on main: scoped _getenv (0f154e7) bakes the correct per-profile home_channel into each profile's config at load time.
  • The Matrix room-level routing is covered by the generic gateway.profile_routes engine (5e65f6d); sender-level (per-MXID) routing has no main equivalent, but the right shape now is user_id support in profile_routes, not a Matrix-only env map.
  • The per-profile session-DB routing (~half the diff, not mentioned in the PR description) targets a REAL live gap — multiplexed turns' session rows all land in the default profile's state.db — but the implementation targets a pre-refactor SessionStore (main has since absorbed the routing-index and metadata consolidation rewrites) and the PR ships no test files.

That session-DB isolation idea is genuinely valuable: if you re-propose it as a focused PR against the current SessionStore with tests, happy to prioritize the review. Thanks for the work here.

@teknium1 teknium1 closed this Jul 16, 2026
@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 P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants