From 308bb56349ac996bbd2c57d1a0885eac24d31c4e Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Mon, 22 Jun 2026 00:24:31 +0800 Subject: [PATCH] fix(gateway): bind profile override in _persist_live_session_system_prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tests/test_tui_gateway_server.py | 86 ++++++++++++++++++++++++++++++++ tui_gateway/server.py | 18 ++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 8799be8afe..ba6bea4bac 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -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" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index a3a1a4ddfd..45805c9de7 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -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