fix(sessions): persist Bot Chat override provenance atomically

This commit is contained in:
fangliquanflq
2026-09-07 18:33:41 +08:00
committed by Teknium
parent a16d45828f
commit 53ac548f36
2 changed files with 30 additions and 9 deletions

View File

@@ -115,6 +115,7 @@ def test_apply_model_switch_does_not_leak_process_env():
}
sess_a = {"agent": _FakeAgent(), "session_key": "k-A", "model_override": None}
persisted_composer_profiles = []
with (
patch("hermes_cli.model_switch.parse_model_flags",
return_value=("glm-5.1", None, False, False, True)),
@@ -125,6 +126,11 @@ def test_apply_model_switch_does_not_leak_process_env():
patch("tui_gateway.server._restart_slash_worker"),
patch("tui_gateway.server._session_info", return_value={}),
patch("hermes_cli.model_switch.persist_model_selection") as mock_persist,
patch(
"tui_gateway.server._persist_live_session_runtime",
side_effect=lambda session: persisted_composer_profiles.append(
session.get("composer_override_profile")),
) as persist_runtime,
patch("tui_gateway.server._config_model_target", return_value=("minimax/m3", "minimax")),
):
before = {k: os.environ.get(k) for k in env_keys}
@@ -140,6 +146,10 @@ def test_apply_model_switch_does_not_leak_process_env():
assert sess_b["model_override"]["model"] == "zai/glm-5.1"
assert sess_b["model_override"]["provider"] == "zai"
assert sess_b["composer_override_profile"] == {"model": "minimax/m3", "provider": "minimax"}
# _commit_agent_switch owns the runtime transaction; provenance must be present
# on its first (and only) DB write rather than relying on a second best-effort write.
persist_runtime.assert_called_once_with(sess_b)
assert persisted_composer_profiles == [{"model": "minimax/m3", "provider": "minimax"}]
# The switched agent mutated in place.
assert sess_b["agent"].model == "zai/glm-5.1"
# Sibling session is completely untouched.

View File

@@ -283,8 +283,26 @@ def _apply_model_switch(
confirm = _expensive_model_confirm(result, current_base_url, current_api_key, agent)
if confirm is not None:
return confirm
if agent:
_commit_agent_switch(sid, session, agent, result, current_model, restore_snapshot)
records_composer_override = (
pin_session_override and isinstance(session, dict) and not one_turn
and not persist_global and session.get("follow_profile_config"))
had_composer_profile = "composer_override_profile" in session
previous_composer_profile = session.get("composer_override_profile")
if records_composer_override:
profile_model, profile_provider = _config_model_target()
session["composer_override_profile"] = {
"model": profile_model, "provider": profile_provider}
try:
if agent:
# Provenance must exist before this transaction persists the switched runtime.
_commit_agent_switch(sid, session, agent, result, current_model, restore_snapshot)
except Exception:
if records_composer_override:
if had_composer_profile:
session["composer_override_profile"] = previous_composer_profile
else:
session.pop("composer_override_profile", None)
raise
# PER-SESSION override so a rebuild of THIS session (/new, resume) re-derives the model.
# Deliberately NOT written to process-global env (HERMES_MODEL & co.): the desktop hosts
# every same-profile session in one process, so os.environ would leak the switch to all.
@@ -292,13 +310,6 @@ def _apply_model_switch(
session["model_override"] = {
"model": result.new_model, "provider": result.target_provider,
"base_url": result.base_url, "api_key": result.api_key, "api_mode": result.api_mode}
if not persist_global and session.get("follow_profile_config"):
profile_model, profile_provider = _config_model_target()
session["composer_override_profile"] = {
"model": profile_model, "provider": profile_provider}
if agent:
# _commit_agent_switch persists before the session pin and its provenance exist.
_persist_live_session_runtime(session)
if persist_global:
from hermes_cli.model_switch import persist_model_selection
persist_model_selection(result)