Skip to content

feat(gateway): /profile <name> switches active profile in messaging platforms - #66519

Closed
mehmetkr-31 wants to merge 1 commit into
NousResearch:mainfrom
mehmetkr-31:feat/gateway-profile-switch
Closed

feat(gateway): /profile <name> switches active profile in messaging platforms#66519
mehmetkr-31 wants to merge 1 commit into
NousResearch:mainfrom
mehmetkr-31:feat/gateway-profile-switch

Conversation

@mehmetkr-31

Copy link
Copy Markdown
Contributor

Problem

The /profile slash command in messaging platforms (Telegram, Discord, Slack) currently only displays the active profile name and home directory. It does not switch profiles.

This contradicts user expectations — the command accepts an optional argument but ignores it on the gateway path. By contrast, /model <name> does switch the model mid-session. /profile <name> should behave analogously.

Use Case

A user operates two businesses from a single Hermes instance via Telegram:

  1. Marketing agency (default profile) — client communication, campaign management, analytics
  2. Short-term apartment rentals (rental profile) — property management, pricing, guest communication, platform integrations

Each profile has its own SOUL.md persona, cron jobs, skills, and memory. Currently, to switch between them in Telegram requires running two separate gateway instances with two Telegram bots — heavy infrastructure for a simple profile switch.

A single-bot /profile rental/profile default workflow is far more ergonomic.

Solution

/profile <name> now switches the active profile:

Command Behavior
/profile Show active profile name and home directory (unchanged)
/profile <name> Switch active profile, close current session, prompt user to start fresh with /new

Why close the session?

A mid-conversation profile switch would invalidate the cached system prompt and tool schemas, breaking the per-conversation prompt-cache contract (AGENTS.md: "Prompt caching is sacred"). Closing the session and prompting /new preserves cache integrity while keeping the UX simple.

Tests

Added 4 new tests in tests/gateway/test_session_boundary_security_state.py:

  • /profile without args shows current profile
  • /profile <name> switches profile and closes session
  • /profile <same-name> is a no-op
  • /profile <nonexistent> returns error without switching

All 9 tests pass.

Fixes #65936

…latforms

The /profile slash command in messaging platforms (Telegram, Discord, Slack)
previously only displayed the active profile name and home directory. It
now accepts an optional profile name to switch to.

- /profile          → show active profile (unchanged)
- /profile <name>   → switch active profile, close current session
                      (to preserve prompt-cache integrity), and prompt
                      user to start fresh with /new

This enables single-bot multi-profile workflows (e.g., /profile rental
→ /profile default) without running separate gateway instances.

Fixes NousResearch#65936
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state needs-decision Awaiting maintainer decision before any implementation labels Jul 17, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: OPEN #61205 and #24914 cover broader source-aware/multiplexed profile routing. This gateway-only approach changes the process active profile and closes the current session, so it needs a maintainer choice on multi-chat behavior rather than a duplicate closure.

@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 focused gateway proposal. The underlying per-chat routing request remains valid, but this implementation needs architectural rework against current main.

Problems

  • gateway/slash_commands.py:369 only calls set_active_profile(), which writes the sticky selection (hermes_cli/profiles.py:1807-1829); it does not alter the gateway runtime HERMES_HOME. Non-multiplexed turns pass straight through at gateway/run.py:17631-17639, so the next /new still runs in the original profile.
  • Current main already resolves multiplexed turns from source.profile (gateway/run.py:17641-17649, 17700-17742) and has source-aware bare /profile behavior at gateway/slash_commands.py:332-377. This diff replaces that handler without binding the source to the target profile.
  • The close path at PR lines 379-382 omits /new's run-generation invalidation and running-agent release (gateway/slash_commands.py:111-117), leaving stale in-flight work unguarded.

Suggested changes

  • Design this as a per-source binding with the open multiplex-routing work (#61205 / #24914), while preserving bare /profile reporting.
  • Reuse the complete session-boundary lifecycle and add a real two-profile gateway test; the current test mocks set_active_profile to a no-op at tests/gateway/test_session_boundary_security_state.py:343.

Automated hermes-sweeper review.

Comment thread gateway/slash_commands.py
return f"✅ Already on profile `{target_profile}`."

try:
set_active_profile(target_profile)

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.

set_active_profile() only writes the sticky default selection; it does not change this running gateway's HERMES_HOME. Since get_active_profile_name() derives from HERMES_HOME and non-multiplexed _run_agent is unscoped, the next /new still runs the original profile. This needs a source/profile runtime binding, not just the sticky-default write.

Comment thread gateway/slash_commands.py
source = event.source
session_key = self._session_key_for_source(source)
current_entry = await self.async_session_store.get_or_create_session(source)
await self.async_session_store.end_session(session_key, reason="profile_switch")

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 boundary needs the same run-generation invalidation and running-agent release used by current /new (gateway/slash_commands.py:111-117); otherwise an old in-flight turn can still complete after the profile/session boundary.

original_exists = profiles_mod.profile_exists
original_set = profiles_mod.set_active_profile
profiles_mod.profile_exists = lambda name: name == "rental"
profiles_mod.set_active_profile = lambda name: None

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 no-op mock proves only the response path. Add a real temporary-profile test that verifies the post-switch gateway turn resolves the target profile's runtime home/config; the current implementation's central failure is that the process runtime scope never changes.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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 area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 18, 2026
@mehmetkr-31

Copy link
Copy Markdown
Contributor Author

Closing this one myself rather than leaving it open.

The sweeper's read was right and I don't think it's salvageable as written: the diff calls set_active_profile(), which only writes the sticky selection — it never alters the gateway runtime HERMES_HOME, so a non-multiplexed turn passes straight through and the next /new still runs in the original profile. The handler it replaces already has source-aware bare /profile behaviour on current main, and the close path skips /new's run-generation invalidation and running-agent release, leaving in-flight work unguarded.

Fixing those properly means designing this as a per-source binding alongside the open multiplex-routing work (#61205 / #24914), not patching this handler — and it would need a real two-profile gateway test rather than the one here, which mocks set_active_profile to a no-op and so proves nothing.

That is a different change from this one, so a fresh PR against that design is the honest path. The underlying request — per-chat profile routing — stays valid and is tracked by those issues.

For the record the branch had also drifted ~3900 commits behind main and gone CONFLICTING, so nothing here is reusable as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have 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 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.

feat(gateway): /profile <name> should switch profiles in Telegram (and other messaging platforms)

3 participants