fix(gateway): /profile reports the profile serving the source, not the multiplexer's

On a multiplexed gateway the process-level active profile is always the
multiplexer's own (usually "default"), so /profile answered "default" in
every chat regardless of which profile actually served it — making
per-chat persona routing look broken when it was working.

Report source.profile (stamped by the /p/<profile>/ URL prefix, a
per-credential adapter, or a room->profile map) and resolve the
displayed home under that profile's runtime scope, mirroring the scoped
/reset banner (#59003). Unstamped sources fall back to the active
profile and default home, so single-profile gateways are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonny Kovacs 2026-07-10 13:37:57 -05:00 committed by Teknium
parent 8191f621c3
commit f7d6f099db
2 changed files with 78 additions and 3 deletions

View file

@ -330,12 +330,33 @@ class GatewaySlashCommandsMixin:
return EphemeralReply(f"{header}{_tip_line}")
async def _handle_profile_command(self, event: MessageEvent) -> str:
"""Handle /profile — show active profile name and home directory."""
"""Handle /profile — show the profile serving this source and its home.
On a multiplexed gateway the process-level active profile is always
the multiplexer's own (usually ``default``), so reporting it would
answer "default" in every chat regardless of which profile actually
serves the room/channel (``source.profile`` stamped by the
``/p/<profile>/`` URL prefix, a per-credential adapter, or a room
profile map). Report the stamped profile and, like the scoped /reset
banner (#59003), resolve the displayed home under that profile's
runtime scope. Single-profile gateways are unchanged: an unstamped
source falls back to the active profile and the default home.
"""
from hermes_constants import display_hermes_home
from hermes_cli.profiles import get_active_profile_name
display = display_hermes_home()
profile_name = get_active_profile_name()
source = getattr(event, "source", None)
profile_name = (
getattr(source, "profile", "") or ""
).strip() or get_active_profile_name()
try:
from gateway.run import _profile_runtime_scope
profile_home = self._resolve_profile_home_for_source(source)
with _profile_runtime_scope(profile_home):
display = display_hermes_home()
except Exception:
display = display_hermes_home()
lines = [
t("gateway.profile.header", profile=profile_name),

View file

@ -672,6 +672,60 @@ async def test_profile_command_reports_custom_root_profile(monkeypatch, tmp_path
assert f"**Home:** `{profile_home}`" in result
@pytest.mark.asyncio
async def test_profile_command_reports_source_stamped_profile(monkeypatch, tmp_path):
"""On a multiplexed gateway, /profile reports the profile SERVING the
source (source.profile URL prefix / per-credential adapter / room map),
not the multiplexer's active profile, which is always the default and
made /profile answer "default" in every persona chat."""
hermes_home = tmp_path / ".hermes"
profile_home = hermes_home / "profiles" / "milo"
profile_home.mkdir(parents=True)
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-1",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner = _make_runner(session_entry)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
event = _make_event("/profile")
event.source.profile = "milo"
result = await runner._handle_profile_command(event)
assert "**Profile:** `milo`" in result
assert f"**Home:** `{profile_home}`" in result
@pytest.mark.asyncio
async def test_profile_command_unstamped_source_unchanged(monkeypatch, tmp_path):
"""Single-profile behavior is untouched: an unstamped source reports the
active profile and the default home."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-1",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner = _make_runner(session_entry)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
result = await runner._handle_profile_command(_make_event("/profile"))
assert "**Profile:** `default`" in result
assert f"**Home:** `{hermes_home}`" in result
@pytest.mark.asyncio
async def test_post_delivery_callback_generation_snapshot_happens_after_bind():
"""Regression: the callback_generation snapshot in _process_message_background