fix(desktop): keep live model switch metadata truthful

A live config.set model switch already moved the next API call to the new model,
but the conversation could still restore an old sessions.system_prompt snapshot
whose Model/Provider lines named the previous runtime. That made "what model are
you?" answer from stale metadata even while inference ran on the new model.

After a live switch we now refresh the stored system prompt and append a real
system-history pivot (not a fake user turn) so the transcript itself records the
new model/provider. Restore also rejects already-stale prompt snapshots when
their Model/Provider lines disagree with the runtime, so existing bad sessions
self-heal.
This commit is contained in:
Brooklyn Nicholson
2026-06-16 09:50:17 -05:00
parent cb6b4127e7
commit 7d938cc5c9
4 changed files with 185 additions and 1 deletions
+42
View File
@@ -29,6 +29,7 @@ def _make_agent(session_db=None, prebuilt_prompt: str = "BUILT_PROMPT"):
agent._cached_system_prompt = None
agent.session_id = "test-session-id"
agent.model = "test-model"
agent.provider = "openrouter"
agent.platform = "cli"
agent._session_db = session_db
agent._build_system_prompt = MagicMock(return_value=prebuilt_prompt)
@@ -67,6 +68,47 @@ class TestStoredPromptReuse:
_restore_or_build_system_prompt(agent, None, [{"role": "user", "content": "hi"}])
assert agent._cached_system_prompt == stored
def test_present_row_with_stale_runtime_identity_rebuilds(self, caplog):
"""Stored prompts are cache gold unless their runtime identity is stale.
A live /model switch updates the agent and DB model_config immediately.
If the old system_prompt snapshot still says the previous model,
blindly restoring it makes the next turn call the new model while the
model reads old `Model:` metadata ("what model are you?" lies).
"""
stored = (
"You are Hermes Agent.\n\n"
"Conversation started: Tuesday, June 16, 2026\n"
"Session ID: test-session-id\n"
"Model: anthropic/claude-opus-4.8-fast\n"
"Provider: openrouter"
)
db = MagicMock()
db.get_session.return_value = {"system_prompt": stored}
agent = _make_agent(
session_db=db,
prebuilt_prompt=(
"You are Hermes Agent.\n\n"
"Conversation started: Tuesday, June 16, 2026\n"
"Session ID: test-session-id\n"
"Model: openai/gpt-5.5\n"
"Provider: openrouter"
),
)
agent.model = "openai/gpt-5.5"
with caplog.at_level(logging.INFO, logger="agent.conversation_loop"):
_restore_or_build_system_prompt(agent, None, [{"role": "user", "content": "hi"}])
assert agent._cached_system_prompt.endswith(
"Model: openai/gpt-5.5\nProvider: openrouter"
)
agent._build_system_prompt.assert_called_once_with(None)
db.update_system_prompt.assert_called_once_with(
agent.session_id, agent._cached_system_prompt
)
assert any("stale runtime identity" in r.getMessage() for r in caplog.records)
# ---------------------------------------------------------------------------
# Legitimate fresh-build paths (no history, no DB)
+42
View File
@@ -3326,12 +3326,39 @@ def test_config_set_model_switches_agent_without_touching_env(monkeypatch):
provider = "openai-codex"
base_url = ""
api_key = ""
session_id = "sid"
_cached_system_prompt = "Model: gpt-5.3-codex\nProvider: openai-codex"
def switch_model(self, **kwargs):
self.model = kwargs["new_model"]
self.provider = kwargs["new_provider"]
def _build_system_prompt(self, _system_message=None):
return f"Model: {self.model}\nProvider: {self.provider}"
class SessionDB:
def __init__(self):
self.model_config = None
self.system_prompt = None
self.messages = []
def get_session(self, _session_id):
return {"model_config": self.model_config}
def update_session_meta(self, _session_id, model_config_json, _model=None):
self.model_config = model_config_json
def update_system_prompt(self, _session_id, system_prompt):
self.system_prompt = system_prompt
def append_message(self, session_id, role, content=None, **_kwargs):
self.messages.append(
{"session_id": session_id, "role": role, "content": content}
)
agent = Agent()
db = SessionDB()
agent._session_db = db
session = _session(agent=agent)
server._sessions["sid"] = session
monkeypatch.setenv("HERMES_TUI_PROVIDER", "openai-codex")
@@ -3373,6 +3400,21 @@ def test_config_set_model_switches_agent_without_touching_env(monkeypatch):
# ...override recorded on the session...
assert session["model_override"]["model"] == "anthropic/claude-sonnet-4.6"
assert session["model_override"]["provider"] == "anthropic"
# ...the persisted prompt snapshot tracks the new runtime identity too.
# Without this, the next turn restored the old system prompt from the DB:
# API calls went to the new model, but "what model are you?" still read
# "Model: old/model" from the stored prompt.
assert db.system_prompt == (
"Model: anthropic/claude-sonnet-4.6\nProvider: anthropic"
)
assert agent._cached_system_prompt == db.system_prompt
assert session["history"][-1]["role"] == "system"
assert "changed to anthropic/claude-sonnet-4.6" in session["history"][-1]["content"]
assert db.messages[-1] == {
"session_id": "session-key",
"role": "system",
"content": session["history"][-1]["content"],
}
# ...and the shared process env was NOT touched.
assert os.environ["HERMES_TUI_PROVIDER"] == "openai-codex"
assert "HERMES_MODEL" not in os.environ