diff --git a/tests/tui_gateway/test_make_agent_provider.py b/tests/tui_gateway/test_make_agent_provider.py index f3e4b8c2a2..bcbe25711f 100644 --- a/tests/tui_gateway/test_make_agent_provider.py +++ b/tests/tui_gateway/test_make_agent_provider.py @@ -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. diff --git a/tui_gateway/model_switch.py b/tui_gateway/model_switch.py index 2b24bb6579..ce6ac65abc 100644 --- a/tui_gateway/model_switch.py +++ b/tui_gateway/model_switch.py @@ -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)