fix(gateway): bind profile override in _persist_live_session_system_prompt

Fixes #50233

_persist_live_session_system_prompt rebuilds the system prompt after a
live model switch (/model), but _start_agent_build's finally block has
already reset set_hermes_home_override by then. load_soul_md() and
build_skills_system_prompt() call get_hermes_home() which falls back to
the root ~/.hermes, loading the wrong SOUL.md identity and skills for
the session's profile.

Fix: set_hermes_home_override(session["profile_home"]) before calling
agent._build_system_prompt() in _persist_live_session_system_prompt,
and reset it in a finally block. Also upgrade the failure log from
DEBUG to WARNING so silent profile-wrong-prompt issues are visible.

The first-prompt lazy-build path is unaffected — _run_prompt_submit
already re-sets the override before run_conversation. The /model slash
worker path does not, which is the gap this fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Max Hsu
2026-06-22 00:24:31 +08:00
committed by Teknium
parent 4d1ef7fbd2
commit 308bb56349
2 changed files with 103 additions and 1 deletions
+86
View File
@@ -18045,3 +18045,89 @@ def test_prompt_submit_unconfirmed_truncation_refuses_before_target_resolution(
assert len(sess["history"]) == 4
finally:
server._sessions.pop(sid, None)
def test_persist_live_session_system_prompt_uses_profile_home(monkeypatch, tmp_path):
"""Issue #50233: _persist_live_session_system_prompt must re-bind
HERMES_HOME to the session's profile before rebuilding the system
prompt. Without this, a /model switch rebuilds the prompt with the
root profile's SOUL.md and skills instead of the session's profile.
"""
profile_home = tmp_path / "profile-work"
profile_home.mkdir()
(profile_home / "SOUL.md").write_text(
"# Work persona\nYou are a work agent.", encoding="utf-8"
)
built_homes = []
class FakeAgent:
model = "test-model"
provider = "test"
_cached_system_prompt = None
_session_db = None
def _build_system_prompt(self, system_message=None):
from hermes_constants import get_hermes_home
home = get_hermes_home()
built_homes.append(str(home))
soul = (
(home / "SOUL.md").read_text(encoding="utf-8")
if (home / "SOUL.md").exists()
else ""
)
return f"System prompt from {home}\n{soul}"
class FakeDB:
def update_system_prompt(self, session_id, prompt):
pass
agent = FakeAgent()
agent._session_db = FakeDB()
session = {
"agent": agent,
"session_key": "test-key",
"profile_home": str(profile_home),
}
server._persist_live_session_system_prompt(session)
# The system prompt must have been built while the override pointed
# to the profile home, not the root ~/.hermes.
assert len(built_homes) == 1, f"expected 1 build, got {built_homes}"
assert str(profile_home) in built_homes[0], (
f"system prompt built with wrong home: {built_homes[0]}"
)
assert "Work persona" in agent._cached_system_prompt
# The override must have been reset after the call.
from hermes_constants import get_hermes_home_override
assert get_hermes_home_override() is None
def test_persist_live_session_system_prompt_no_profile_is_unchanged(monkeypatch):
"""Sessions without a profile_home must not set/clear any override —
the function should behave identically to before the fix."""
class FakeAgent:
model = "test"
_cached_system_prompt = None
_session_db = None
def _build_system_prompt(self, system_message=None):
return "plain prompt"
class FakeDB:
def update_system_prompt(self, session_id, prompt):
pass
agent = FakeAgent()
agent._session_db = FakeDB()
session = {
"agent": agent,
"session_key": "test-key",
"profile_home": None,
}
# Should not raise, should still build and cache.
server._persist_live_session_system_prompt(session)
assert agent._cached_system_prompt == "plain prompt"
+17 -1
View File
@@ -4016,12 +4016,28 @@ def _persist_live_session_system_prompt(session: dict | None) -> None:
if db is None or not hasattr(db, "update_system_prompt"):
return
# Re-bind HERMES_HOME to the session's profile so load_soul_md() and
# build_skills_system_prompt() resolve to the correct profile. Without
# this, _start_agent_build's finally block has already reset the
# override and the rebuilt prompt silently uses the root profile's
# SOUL.md and skills. See issue #50233.
profile_home = session.get("profile_home")
home_token = (
set_hermes_home_override(profile_home) if profile_home else None
)
try:
prompt = agent._build_system_prompt(None)
agent._cached_system_prompt = prompt
db.update_system_prompt(getattr(agent, "session_id", None) or session_key, prompt)
except Exception:
logger.debug("failed to persist live session system prompt", exc_info=True)
logger.warning(
"failed to persist live session system prompt for session %s",
session_key,
exc_info=True,
)
finally:
if home_token is not None:
reset_hermes_home_override(home_token)
# Stable leading text of the model-switch marker, shared by the builder and the