{"repo":"NousResearch/hermes-agent","public_id":"23131","display_label":"Session reset ignoring config default model","summary":"The /new and /reset commands did not reload the default model from config.yaml, instead retaining in-session model switches. Multiple patches address session scoping and config reloading behavior.","subsystem":"hermes_cli","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":2,"closed_issues":2,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":67979,"merged_at":"2026-07-20T10:27:23+00:00"},"resolution":{"state":"resolved","by":"pr-67979"},"freshness":{"state":"verified","last_verified_at":"2026-07-20T10:27:23+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3573333333333333},"fix_edges":[{"issue_number":23131,"pr_number":67979,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-20T10:27:23+00:00"},{"issue_number":48055,"pr_number":67979,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-20T10:27:23+00:00"}],"members":[{"kind":"issue","number":23131,"title":"Bug: /reset and /new do not reload default model from config.yaml","url":null,"state":"closed"},{"kind":"issue","number":48055,"title":"/new does not reset model to config default after session-only /model switch","url":null,"state":"closed"},{"kind":"pr","number":23182,"title":"fix: reload default model from config.yaml on /reset and /new","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#23131","number":23131}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":23262,"title":"fix(cli): reload configured default model on new session","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#23182","number":23182},{"kind":"pr","value":"#23131","number":23131}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":23342,"title":"fix(cli): reload default model from config on /reset and /new","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#23182","number":23182},{"kind":"pr","value":"#23131","number":23131}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":67979,"title":"fix: session-scoped /fast + full /new reset to config defaults","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","gateway/run.py","gateway/slash_commands.py","hermes_cli/cli_commands_mixin.py","hermes_cli/commands.py","tests/cli/test_fast_command.py","tests/cli/test_reasoning_command.py","tests/gateway/test_choice_picker.py","tests/gateway/test_fast_command.py","tests/test_tui_gateway_server.py","tui_gateway/server.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -7154,6 +7154,68 @@ def new_session(self, silent=False, title=None):\n         self.reasoning_config = _parse_reasoning_config(\n             CLI_CONFIG[\"agent\"].get(\"reasoning_effort\", \"\")\n         )\n+        # /new is a full conversation boundary: session-scoped runtime\n+        # overrides (/model --session, /fast, one-turn restores) do not carry\n+        # forward.  Re-derive model/provider and service tier from config.yaml\n+        # so a session-only switch never leaks into the next session (#48055,\n+        # #23131).\n+        self._pending_one_turn_model_restore = None\n+        self.service_tier = _parse_service_tier_config(\n+            CLI_CONFIG[\"agent\"].get(\"service_tier\", \"\")\n+        )\n+        _model_config = CLI_CONFIG.get(\"model\", {})\n+        _config_model = (\n+            (_model_config.get(\"default\") or _model_config.get(\"model\") or \"\")\n+            if isinstance(_model_config, dict)\n+            else (_model_config or \"\")\n+        )\n+        if _config_model and _config_model != getattr(self, \"model\", None):\n+            _config_provider = (\n+                _model_config.get(\"provider\", \"\")\n+                if isinstance(_model_config, dict)\n+                else \"\"\n+            )\n+            try:\n+                from hermes_cli.model_switch import switch_model as _switch_model\n+\n+                _reset_result = _switch_model(\n+                    raw_input=_config_model,\n+                    current_provider=self.provider or \"\",\n+                    current_model=self.model or \"\",\n+                    current_base_url=self.base_url or \"\",\n+                    current_api_key=self.api_key or \"\",\n+                    is_global=False,\n+                    explicit_provider=_config_provider or \"\",\n+                )\n+                if _reset_result.success:\n+                    if self.agent:\n+                        self.agent.switch_model(\n+                            new_model=_reset_result.new_model,\n+                            new_provider=_reset_result.target_provider,\n+                            api_key=[redacted:secret-assignment]\n+                            base_url=_reset_result.base_url,\n+                            api_mode=_reset_result.api_mode,\n+                        )\n+                    self.model = _reset_result.new_model\n+                    self.provider = _reset_result.target_provider\n+                    self.requested_provider = _reset_result.target_provider\n+                    self._explicit_api_key = [redacted:secret-assignment]\n+                    self._explicit_base_url = _reset_result.base_url\n+                    if _reset_result.api_key:\n+                        self.api_key = [redacted:secret-assignment]\n+                    if _reset_result.base_url:\n+                        self.base_url = _reset_result.base_url\n+                    if _reset_result.api_mode:\n+                        self.api_mode = _reset_result.api_mode\n+                    if not silent:\n+                        _cprint(\n+                            f\"  (model reset to config default: \"\n+                            f\"{_reset_result.new_model})\"\n+                        )\n+            except Exception:\n+                # Best-effort: an unreachable config default must never block\n+                # /new. The session keeps the current working model.\n+                logger.debug(\"/new model reset to config default failed\", exc_info=True)\n         _sync_process_session_id(self.session_id)\n \n         if self.agent:"]},{"file":"gateway/run.py","hunks":["@@ -2031,6 +2031,7 @@ def _own_policy_open_startup_violation(config) -> Optional[str]:\n     \"_session_model_overrides\",\n     \"_pending_one_turn_model_restores\",\n     \"_session_reasoning_overrides\",\n+    \"_session_service_tier_overrides\",\n     \"_pending_model_notes\",\n     \"_last_resolved_model\",\n     \"_queued_events\",","@@ -3055,6 +3056,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew\n     _session_model_overrides: Dict[str, Dict[str, str]] = {}\n     _pending_one_turn_model_restores: Dict[str, Dict[str, Any]] = {}\n     _session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}\n+    _session_service_tier_overrides: Dict[str, Optional[str]] = {}\n     _pending_turn_sidecar_notes: Dict[str, List[str]] = {}\n     _session_ephemeral_pin: Dict[str, tuple] = {}\n     _session_vc_last: Dict[str, str] = {}","@@ -3264,6 +3266,9 @@ def __init__(self, config: Optional[GatewayConfig] = None):\n         # Per-session reasoning effort overrides from /reasoning.\n         # Key: session_key, Value: parsed reasoning config dict.\n         self._session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}\n+        # Per-session fast-mode overrides from /fast.\n+        # Key: session_key, Value: \"priority\" or None (explicit normal).\n+        self._session_service_tier_overrides: Dict[str, Optional[str]] = {}\n         # Per-turn must-deliver notes relocated out of the ephemeral system\n         # prompt (auto-reset note, first-contact intro, voice-channel change).\n         # Staged by _handle_message_with_agent, consumed once by run_sync and","@@ -5238,6 +5243,52 @@ def _set_session_reasoning_override(\n         else:\n             self._session_reasoning_overrides[session_key] = dict(reasoning_config)\n \n+    def _resolve_session_service_tier(\n+        self,\n+        source=None,\n+        session_key: Optional[str] = None,\n+    ) -> Optional[str]:\n+        \"\"\"Resolve the effective service tier for a session.\n+\n+        A session-scoped /fast override wins over the config default. The\n+        override dict stores \"priority\" or None (explicit normal), so key\n+        presence — not value truthiness — decides whether it applies.\n+        \"\"\"\n+        resolved_session_key = session_key\n+        if not resolved_session_key and source is not None:\n+            try:\n+                resolved_session_key = self._session_key_for_source(source)\n+            except Exception:\n+                resolved_session_key = None\n+\n+        overrides = getattr(self, \"_session_service_tier_overrides\", {}) or {}\n+        if resolved_session_key and resolved_session_key in overrides:\n+            return overrides[resolved_session_key]\n+        return self._load_service_tier()\n+\n+    def _set_session_service_tier_override(\n+        self,\n+        session_key: str,\n+        service_tier,\n+        clear: bool = False,\n+    ) -> None:\n+        \"\"\"Set or clear the session-scoped /fast override.\n+\n+        ``service_tier`` is \"priority\" or None (explicit normal). Pass\n+        ``clear=True`` to remove the override entirely (fall back to config).\n+        \"\"\"\n+        if not session_key:\n+            return\n+        if \"_session_service_tier_overrides\" not in self.__dict__:\n+            # Force an instance-level dict: the class attribute is a shared\n+            # default for partially-constructed test runners, and mutating it\n+            # would leak overrides across runner instances.\n+            self._session_service_tier_overrides = {}\n+        if clear:\n+            self._session_service_tier_overrides.pop(session_key, None)\n+        else:\n+            self._session_service_tier_overrides[session_key] = service_tier\n+\n     @staticmethod\n     def _load_service_tier() -> str | None:\n         \"\"\"Load Priority Processing setting from config.yaml.","@@ -14702,7 +14753,7 @@ async def _run_background_task_inner(\n                 source=source, model=model\n             )\n             self._reasoning_config = reasoning_config\n-            self._service_tier = self._load_service_tier()\n+            self._service_tier = self._resolve_session_service_tier(source=source)\n             turn_route = self._resolve_turn_agent_config(prompt, model, runtime_kwargs)\n \n             # Enrich the prompt with image descriptions so the background","@@ -19917,7 +19968,9 @@ def run_sync():\n                 model=model,\n             )\n             self._reasoning_config = reasoning_config\n-            self._service_tier = self._load_service_tier()\n+            self._service_tier = self._resolve_session_service_tier(\n+                source=source, session_key=session_key\n+            )\n             # Set up stream consumer for token streaming or interim commentary.\n             _stream_consumer = None\n             _stream_delta_cb = None"]},{"file":"gateway/slash_commands.py","hunks":["@@ -3108,43 +3108,64 @@ def _set_approval(enabled: bool):\n         return out\n \n     async def _handle_fast_command(self, event: MessageEvent) -> Optional[str]:\n-        \"\"\"Handle /fast — mirror the CLI Priority Processing toggle in gateway chats.\"\"\"\n+        \"\"\"Handle /fast — mirror the CLI Priority Processing toggle in gateway chats.\n+\n+        Session-scoped by default; ``--global`` persists agent.service_tier\n+        to config.yaml (parity with /model and /reasoning).\n+        \"\"\"\n         from gateway.run import _load_gateway_config, _resolve_gateway_model\n         from hermes_cli.models import model_supports_fast_mode\n \n-        args = event.get_command_args().strip().lower()\n-        self._service_tier = self._load_service_tier()\n+        raw_args = event.get_command_args().strip().lower()\n+        # Reuse the /reasoning arg parser: strips --global (any position),\n+        # normalizes unicode dashes.\n+        args, persist_global = self._parse_reasoning_command_args(raw_args)\n+        session_key = self._session_key_for_source(event.source)\n+        self._service_tier = self._resolve_session_service_tier(\n+            session_key=session_key\n+        )\n \n         user_config = _load_gateway_config()\n         model = _resolve_gateway_model(user_config)\n         if not model_supports_fast_mode(model):\n             return t(\"gateway.fast.not_supported\")\n \n-        def _apply_fast_selection(value: str) -> str:\n+        def _apply_fast_selection(value: str, persist: bool = False) -> str:\n             \"\"\"Apply a /fast argument (typed or picked) and return the reply.\"\"\"\n             if value in {\"fast\", \"on\"}:\n-                self._service_tier = \"priority\"\n+                tier = \"priority\"\n                 saved_value = \"fast\"\n                 label = t(\"gateway.fast.label_fast\")\n             elif value in {\"normal\", \"off\"}:\n-                self._service_tier = None\n+                tier = None\n                 saved_value = \"normal\"\n                 label = t(\"gateway.fast.label_normal\")\n             else:\n                 return t(\"gateway.fast.unknown_arg\", arg=value)\n-            if self._save_gateway_config_key(\"agent.service_tier\", saved_value):\n-                return t(\"gateway.fast.saved\", label=label)\n+            self._service_tier = tier\n+            if persist:\n+                if self._save_gateway_config_key(\"agent.service_tier\", saved_value):\n+                    # Global write supersedes any session override.\n+                    self._set_session_service_tier_override(\n+                        session_key, None, clear=True\n+                    )\n+                    self._evict_cached_agent(session_key)\n+                    return t(\"gateway.fast.saved\", label=label)\n+                # Config write failed — fall back to a session override so the\n+                # user's choice still applies (mirrors /reasoning --global).\n+                self._set_session_service_tier_override(session_key, tier)\n+                self._evict_cached_agent(session_key)\n+                return t(\"gateway.fast.session_only\", label=label)\n+            self._set_session_service_tier_override(session_key, tier)\n+            self._evict_cached_agent(session_key)\n             return t(\"gateway.fast.session_only\", label=label)\n \n         if not args or args == \"status\":\n             is_fast = self._service_tier == \"priority\"\n             status = t(\"gateway.fast.status_fast\") if is_fast else t(\"gateway.fast.status_normal\")\n \n-            # Interactive picker on platforms that support it.\n-            session_key = self._session_key_for_source(event.source)\n-\n             async def _on_fast_choice(_chat_id: str, value: str) -> str:\n-                return _apply_fast_selection(value)\n+                return _apply_fast_selection(value, persist=persist_global)\n \n             picker_sent = await self._try_send_choice_picker(\n                 event,","@@ -3169,7 +3190,7 @@ async def _on_fast_choice(_chat_id: str, value: str) -> str:\n \n             return t(\"gateway.fast.status\", mode=status)\n \n-        return _apply_fast_selection(args)\n+        return _apply_fast_selection(args, persist=persist_global)\n \n     async def _handle_yolo_command(self, event: MessageEvent) -> Union[str, EphemeralReply]:\n         \"\"\"Handle /yolo — toggle dangerous command approval bypass for this session only.\"\"\""]},{"file":"hermes_cli/cli_commands_mixin.py","hunks":["@@ -2653,7 +2653,11 @@ def _handle_busy_command(self, cmd: str):\n             _cprint(f\"  {_ACCENT}✓ Busy input mode set to '{arg}' (session only){_RST}\")\n \n     def _handle_fast_command(self, cmd: str):\n-        \"\"\"Handle /fast — toggle fast mode (OpenAI Priority Processing / Anthropic Fast Mode).\"\"\"\n+        \"\"\"Handle /fast — toggle fast mode (OpenAI Priority Processing / Anthropic Fast Mode).\n+\n+        Session-scoped by default; ``--global`` persists agent.service_tier\n+        to config.yaml (parity with /model and /reasoning).\n+        \"\"\"\n         from cli import _ACCENT, _DIM, _RST, _cprint, save_config_value\n         if not self._fast_command_available():\n             _cprint(\"  (._.) /fast is only available for models that support fast mode (OpenAI Priority Processing or Anthropic Fast Mode).\")","@@ -2672,10 +2676,15 @@ def _handle_fast_command(self, cmd: str):\n         if len(parts) < 2 or parts[1].strip().lower() == \"status\":\n             status = \"fast\" if self.service_tier == \"priority\" else \"normal\"\n             _cprint(f\"  {_ACCENT}{feature_name}: {status}{_RST}\")\n-            _cprint(f\"  {_DIM}Usage: /fast [normal|fast|status]{_RST}\")\n+            _cprint(f\"  {_DIM}Usage: /fast [normal|fast|status] [--global]{_RST}\")\n             return\n \n-        arg = parts[1].strip().lower()\n+        arg_tokens = [redacted:secret-assignment]\n+        explicit_global = \"--global\" in arg_tokens\n+        arg = \" \".join(\n+            token for token in arg_tokens\n+            if token not in (\"--global\", \"--session\")\n+        )\n \n         if arg in {\"fast\", \"on\"}:\n             self.service_tier = \"priority\"","@@ -2687,14 +2696,16 @@ def _handle_fast_command(self, cmd: str):\n             label = \"NORMAL\"\n         else:\n             _cprint(f\"  {_DIM}(._.) Unknown argument: {arg}{_RST}\")\n-            _cprint(f\"  {_DIM}Usage: /fast [normal|fast|status]{_RST}\")\n+            _cprint(f\"  {_DIM}Usage: /fast [normal|fast|status] [--global]{_RST}\")\n             return\n \n         self.agent = None  # Force agent re-init with new service-tier config\n-        if save_config_value(\"agent.service_tier\", saved_value):\n+        if explicit_global and save_config_value(\"agent.service_tier\", saved_value):\n             _cprint(f\"  {_ACCENT}✓ {feature_name} set to {label} (saved to config){_RST}\")\n+        elif explicit_global:\n+            _cprint(f\"  {_ACCENT}✓ {feature_name} set to {label} (session only; config save failed){_RST}\")\n         else:\n-            _cprint(f\"  {_ACCENT}✓ {feature_name} set to {label} (session only){_RST}\")\n+            _cprint(f\"  {_ACCENT}✓ {feature_name} set to {label} (this session — use --global to persist){_RST}\")\n \n     def _handle_debug_command(self, cmd_original: str = \"\"):\n         \"\"\"Handle /debug — upload debug report + logs and print share URLs."]},{"file":"hermes_cli/commands.py","hunks":["@@ -156,8 +156,8 @@ class CommandDef:\n                args_hint=\"[level|show|hide|full|clamp] [--global]\",\n                subcommands=(\"none\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\", \"ultra\", \"show\", \"hide\", \"on\", \"off\", \"full\", \"clamp\", \"--global\")),\n     CommandDef(\"fast\", \"Toggle fast mode — OpenAI Priority Processing / Anthropic Fast Mode (Normal/Fast)\", \"Configuration\",\n-               args_hint=\"[normal|fast|status]\",\n-               subcommands=(\"normal\", \"fast\", \"status\", \"on\", \"off\")),\n+               args_hint=\"[normal|fast|status] [--global]\",\n+               subcommands=(\"normal\", \"fast\", \"status\", \"on\", \"off\", \"--global\")),\n     CommandDef(\"skin\", \"Show or change the display skin/theme\", \"Configuration\",\n                cli_only=True, args_hint=\"[name]\"),\n     CommandDef(\"indicator\", \"Pick the TUI busy-indicator style\", \"Configuration\","]},{"file":"tests/cli/test_fast_command.py","hunks":["@@ -83,6 +83,20 @@ def test_normal_argument_clears_service_tier(self):\n         ):\n             cli_mod.HermesCLI._handle_fast_command(stub, \"/fast normal\")\n \n+        # Session-scoped by default: no config write.\n+        mock_save.assert_not_called()\n+        self.assertIsNone(stub.service_tier)\n+        self.assertIsNone(stub.agent)\n+\n+    def test_global_flag_persists_service_tier(self):\n+        cli_mod = _import_cli()\n+        stub = self._make_cli(service_tier=\"priority\")\n+        with (\n+            patch.object(cli_mod, \"_cprint\"),\n+            patch.object(cli_mod, \"save_config_value\", return_value=True) as mock_save,\n+        ):\n+            cli_mod.HermesCLI._handle_fast_command(stub, \"/fast normal --global\")\n+\n         mock_save.assert_called_once_with(\"agent.service_tier\", \"normal\")\n         self.assertIsNone(stub.service_tier)\n         self.assertIsNone(stub.agent)"]},{"file":"tests/cli/test_reasoning_command.py","hunks":["@@ -220,6 +220,63 @@ def test_new_session_clears_session_reasoning_override(self):\n         self.assertEqual(agent.reasoning_config, {\"enabled\": True, \"effort\": \"medium\"})\n         agent.reset_session_state.assert_called_once()\n \n+    def test_new_session_resets_service_tier_and_model_from_config(self):\n+        \"\"\"/new re-derives service tier and model from config.yaml — session\n+        /fast and /model switches do not carry forward (#48055, #23131).\"\"\"\n+        from cli import CLI_CONFIG, HermesCLI\n+\n+        agent = SimpleNamespace(\n+            reasoning_config=None,\n+            reset_session_state=MagicMock(),\n+            switch_model=MagicMock(),\n+        )\n+        stub = SimpleNamespace(\n+            agent=agent,\n+            conversation_history=[],\n+            session_id=\"old-session\",\n+            _session_db=None,\n+            _pending_title=None,\n+            _resumed=False,\n+            reasoning_config=None,\n+            _notify_session_boundary=MagicMock(),\n+            # Session had switched to fast + a session-only model.\n+            service_tier=\"priority\",\n+            _pending_one_turn_model_restore={\"model\": \"stale\"},\n+            model=\"session-switched-model\",\n+            provider=\"openrouter\",\n+            requested_provider=\"openrouter\",\n+            api_key=\"k\",\n+            base_url=\"\",\n+            api_mode=\"\",\n+        )\n+\n+        fake_result = SimpleNamespace(\n+            success=True,\n+            new_model=\"config-default-model\",\n+            target_provider=\"openrouter\",\n+            api_key=\"k2\",\n+            base_url=\"https://openrouter.ai/api/v1\",\n+            api_mode=\"chat_completions\",\n+        )\n+        with patch.dict(\n+            CLI_CONFIG.setdefault(\"agent\", {}),\n+            {\"reasoning_effort\": \"medium\", \"service_tier\": \"normal\"},\n+        ), patch.dict(\n+            CLI_CONFIG,\n+            {\"model\": {\"default\": \"config-default-model\", \"provider\": \"openrouter\"}},\n+        ), patch(\n+            \"hermes_cli.model_switch.switch_model\", return_value=fake_result\n+        ):\n+            HermesCLI.new_session(stub, silent=True)\n+\n+        # Fast override cleared back to config default (normal → None).\n+        self.assertIsNone(stub.service_tier)\n+        # One-turn restore snapshot cleared.\n+        self.assertIsNone(stub._pending_one_turn_model_restore)\n+        # Model reset to the config default via the live agent swap.\n+        self.assertEqual(stub.model, \"config-default-model\")\n+        agent.switch_model.assert_called_once()\n+\n \n # ---------------------------------------------------------------------------\n # Reasoning extraction and result dict"]},{"file":"tests/gateway/test_choice_picker.py","hunks":["@@ -189,7 +189,8 @@ async def test_bare_fast_sends_picker_when_adapter_supports_it(self, tmp_path, m\n         assert values == [\"fast\", \"normal\"]\n \n     @pytest.mark.asyncio\n-    async def test_fast_picker_selection_persists_service_tier(self, tmp_path, monkeypatch):\n+    async def test_fast_picker_selection_is_session_scoped(self, tmp_path, monkeypatch):\n+        \"\"\"A bare /fast picker tap applies a session override, not a config write.\"\"\"\n         self._patch_fast_support(monkeypatch, tmp_path)\n         adapter = _PickerAdapter()\n         runner = _make_runner(adapter)","@@ -199,6 +200,22 @@ async def test_fast_picker_selection_persists_service_tier(self, tmp_path, monke\n         on_choice = adapter.calls[0][\"on_choice_selected\"]\n         await on_choice(event.source.chat_id, \"fast\")\n \n+        assert runner._service_tier == \"priority\"\n+        assert runner._session_service_tier_overrides\n+        assert not (tmp_path / \"config.yaml\").exists()\n+\n+    @pytest.mark.asyncio\n+    async def test_fast_picker_global_flag_persists_service_tier(self, tmp_path, monkeypatch):\n+        \"\"\"A /fast --global picker tap persists agent.service_tier to config.\"\"\"\n+        self._patch_fast_support(monkeypatch, tmp_path)\n+        adapter = _PickerAdapter()\n+        runner = _make_runner(adapter)\n+        event = _make_event(\"/fast --global\")\n+\n+        await runner._handle_fast_command(event)\n+        on_choice = adapter.calls[0][\"on_choice_selected\"]\n+        await on_choice(event.source.chat_id, \"fast\")\n+\n         assert runner._service_tier == \"priority\"\n         saved = yaml.safe_load((tmp_path / \"config.yaml\").read_text(encoding=\"utf-8\"))\n         assert saved[\"agent\"][\"service_tier\"] == \"fast\""]},{"file":"tests/gateway/test_fast_command.py","hunks":["@@ -143,7 +143,8 @@ def test_turn_route_skips_priority_processing_for_unsupported_models():\n \n \n @pytest.mark.asyncio\n-async def test_handle_fast_command_persists_config(monkeypatch, tmp_path):\n+async def test_handle_fast_command_session_scoped_by_default(monkeypatch, tmp_path):\n+    \"\"\"Bare /fast fast applies a session override — config.yaml untouched.\"\"\"\n     runner = _make_runner()\n \n     monkeypatch.setattr(gateway_run, \"_hermes_home\", tmp_path)","@@ -152,11 +153,57 @@ async def test_handle_fast_command_persists_config(monkeypatch, tmp_path):\n \n     response = await runner._handle_fast_command(_make_event(\"/fast fast\"))\n \n+    assert \"FAST\" in response\n+    assert runner._service_tier == \"priority\"\n+    # Session override recorded; config.yaml NOT written.\n+    assert runner._session_service_tier_overrides\n+    assert not (tmp_path / \"config.yaml\").exists()\n+\n+\n[redacted:email]\n+async def test_handle_fast_command_global_flag_persists_config(monkeypatch, tmp_path):\n+    runner = _make_runner()\n+\n+    monkeypatch.setattr(gateway_run, \"_hermes_home\", tmp_path)\n+    monkeypatch.setattr(gateway_run, \"_load_gateway_config\", lambda: {})\n+    monkeypatch.setattr(gateway_run, \"_resolve_gateway_model\", lambda config=None: \"gpt-5.4\")\n+\n+    response = await runner._handle_fast_command(_make_event(\"/fast fast --global\"))\n+\n     assert \"FAST\" in response\n     assert runner._service_tier == \"priority\"\n \n     saved = yaml.safe_load((tmp_path / \"config.yaml\").read_text(encoding=\"utf-8\"))\n     assert saved[\"agent\"][\"service_tier\"] == \"fast\"\n+    # Global write supersedes the session override.\n+    assert not runner._session_service_tier_overrides\n+\n+\n[redacted:email]\n+async def test_session_fast_override_beats_config_default(monkeypatch, tmp_path):\n+    \"\"\"A session /fast normal wins over agent.service_tier: fast in config.\"\"\"\n+    runner = _make_runner()\n+\n+    monkeypatch.setattr(gateway_run, \"_hermes_home\", tmp_path)\n+    monkeypatch.setattr(gateway_run, \"_load_gateway_config\", lambda: {})\n+    monkeypatch.setattr(\n+        gateway_run,\n+        \"_load_gateway_runtime_config\",\n+        lambda: {\"agent\": {\"service_tier\": \"fast\"}},\n+    )\n+    monkeypatch.setattr(gateway_run, \"_resolve_gateway_model\", lambda config=None: \"gpt-5.4\")\n+\n+    event = _make_event(\"/fast normal\")\n+    session_key = runner._session_key_for_source(event.source)\n+\n+    response = await runner._handle_fast_command(event)\n+\n+    assert \"NORMAL\" in response\n+    # Override stores explicit None (normal) and wins over config \"fast\".\n+    assert session_key in runner._session_service_tier_overrides\n+    assert runner._resolve_session_service_tier(session_key=session_key) is None\n+    # A different session still gets the config default.\n+    assert runner._resolve_session_service_tier(session_key=\"other-session\") == \"priority\"\n \n \n @pytest.mark.asyncio"]},{"file":"tests/test_tui_gateway_server.py","hunks":["@@ -10459,16 +10459,20 @@ def fake_make_agent(sid, key, session_id=None, session_db=None, **kwargs):\n # ── billing/subscription state + error serialization ─────────────────\n \n \n-def test_reset_session_agent_preserves_explicit_normal_fast(monkeypatch):\n+def test_reset_session_agent_clears_session_overrides(monkeypatch):\n+    \"\"\"/new is a full conversation boundary: session-scoped /model, /reasoning,\n+    and /fast overrides do NOT carry into the fresh agent — it re-derives\n+    everything from config.yaml (#48055, #23131).\"\"\"\n     captured = {}\n     new_agent = types.SimpleNamespace(model=\"openai/gpt-5.4\", service_tier=\"\")\n     session = _session(\n         agent=types.SimpleNamespace(\n             model=\"openai/gpt-5.4\",\n-            reasoning_config=None,\n+            reasoning_config={\"enabled\": True, \"effort\": \"high\"},\n             service_tier=\"\",\n         ),\n         model_override={\"model\": \"openai/gpt-5.4\"},\n+        create_reasoning_override={\"enabled\": True, \"effort\": \"high\"},\n         create_service_tier_override=\"\",\n     )\n ","@@ -10488,7 +10492,14 @@ def make_agent(*_args, **kwargs):\n \n     server._reset_session_agent(\"sid\", session)\n \n-    assert captured[\"service_tier_override\"] == \"\"\n+    # No session overrides forwarded — fresh agent builds from config.\n+    assert \"model_override\" not in captured\n+    assert \"reasoning_config_override\" not in captured\n+    assert \"service_tier_override\" not in captured\n+    # And the session pins are gone so a later rebuild can't resurrect them.\n+    assert \"model_override\" not in session\n+    assert \"create_reasoning_override\" not in session\n+    assert \"create_service_tier_override\" not in session\n     assert session[\"agent\"] is new_agent\n \n "]},{"file":"tui_gateway/server.py","hunks":["@@ -4765,25 +4765,22 @@ def tool_progress(event_type: str, name: str | None = None, preview: str | None\n def _reset_session_agent(sid: str, session: dict) -> dict:\n     tokens = [redacted:secret-assignment]\"session_key\"])\n     try:\n-        # Preserve this session's chosen model AND reasoning across /new so a\n-        # reset doesn't silently revert to global config (or to a model\n-        # another session set). See the cross-session-contamination note in\n-        # _apply_model_switch.\n-        reset_kw = {\"model_override\": session.get(\"model_override\")}\n-        old_reasoning = getattr(session.get(\"agent\"), \"reasoning_config\", None)\n-        if old_reasoning is None:\n-            old_reasoning = session.get(\"create_reasoning_override\")\n-        if isinstance(old_reasoning, dict):\n-            reset_kw[\"reasoning_config_override\"] = old_reasoning\n-        create_service_tier_override = session.get(\"create_service_tier_override\")\n-        if create_service_tier_override is not None:\n-            reset_kw[\"service_tier_override\"] = create_service_tier_override\n+        # /new is a full conversation boundary: session-scoped runtime\n+        # overrides (/model, /reasoning, /fast) do NOT carry forward — the\n+        # fresh agent re-derives model/provider, reasoning, and service tier\n+        # from config.yaml (#48055, #23131). Session pins are cleared below so\n+        # a rebuild can't resurrect them. (Global process state is still never\n+        # touched — see the cross-session-contamination note in\n+        # _apply_model_switch.)\n+        session.pop(\"model_override\", None)\n+        session.pop(\"create_reasoning_override\", None)\n+        session.pop(\"create_service_tier_override\", None)\n+        session.pop(\"one_turn_model_restore\", None)\n         new_agent = _make_agent(\n             sid,\n             session[\"session_key\"],\n             session_id=session[\"session_key\"],\n             platform_override=_session_source(session),\n-            **reset_kw,\n         )\n     finally:\n         _clear_session_context(tokens)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}