# NousResearch/hermes-agent · Fix /model command scope in messaging gateways

## Open now

- Nothing open on GitHub.

These issues and PRs address a bug where the /model command defaults to global configuration instead of session scope when used in messaging gateway contexts. The work involves correcting scoping behavior for /model and related commands across different surfaces.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #67946. Verify it addresses your case.
- **Recommended:** read PR #67946 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-67946`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #61458: \[Bug\]: /model command defaults to global config in messaging gateways

## Prior & competing PRs

- **#67946: fix: session-scoped /model and /reasoning defaults across all surfaces**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `gateway/slash_commands.py`, `hermes_cli/cli_commands_mixin.py`, `hermes_cli/commands.py`, `hermes_cli/model_switch.py`, `tests/cli/test_reasoning_command.py`, `tests/gateway/test_25107_stale_base_url_api_mode.py`, `tests/gateway/test_model_command_flat_string_config.py`, `tests/gateway/test_model_picker_persist.py`, `tests/hermes_cli/test_model_switch_persist_default.py`, `tests/hermes_cli/test_reasoning_full_command.py`, `tests/test_tui_gateway_server.py`, `tui_gateway/server.py`, `ui-tui/src/__tests__/createSlashHandler.test.ts`, `ui-tui/src/app/slash/commands/session.ts`, `website/docs/reference/cli-commands.md`, `website/docs/user-guide/configuration.md`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -7151,11 +7151,15 @@ def new_session(self, silent=False, title=None):
             self.conversation_history = []
             self._pending_title = None
             self._resumed = False
    +        self.reasoning_config = _parse_reasoning_config(
    +            CLI_CONFIG["agent"].get("reasoning_effort", "")
    +        )
             _sync_process_session_id(self.session_id)
     
             if self.agent:
                 self.agent.session_id = self.session_id
                 self.agent.session_start = self.session_start
    +            self.agent.reasoning_config = self.reasoning_config
                 self.agent.reset_session_state()
                 if hasattr(self.agent, "_last_flushed_db_idx"):
                     self.agent._last_flushed_db_idx = 0
    ```

    ```diff
    @@ -8189,16 +8193,16 @@ def _handle_model_switch(self, cmd_original: str):
     
             Supports:
               /model                              — show current model + usage hints
    -          /model <name>                       — switch model (persists by default)
    +          /model <name>                       — switch model (this session only)
               /model <name> --once                — switch for the next turn only
    -          /model <name> --session             — switch for this session only
    -          /model <name> --global              — switch and persist (explicit)
    +          /model <name> --session             — switch for this session only (explicit)
    +          /model <name> --global              — switch and persist to config.yaml
               /model <name> --provider <provider> — switch provider + model
               /model --provider <provider>        — switch to provider, auto-detect model
     
    -        Persistence defaults to on (``model.persist_switch_by_default`` in
    -        config.yaml, default True). Use ``--session`` for this CLI session or
    -        ``--once`` for the next turn only.
    +        Persistence defaults to off (``model.persist_switch_by_default`` in
    +        config.yaml, default False — switches are session-scoped). Use
    +        ``--global`` to persist, or ``--once`` for the next turn only.
             """
             from hermes_cli.model_switch import (
                 switch_model,
    ```

    ```diff
    @@ -8225,11 +8229,14 @@ def _handle_model_switch(self, cmd_original: str):
             if one_turn and not model_input and not explicit_provider:
                 _cprint("  ✗ /model --once requires a model or provider.")
                 return
    -        # Resolve the effective persistence once: --session overrides the
    -        # config-gated default, --global forces persist, otherwise defer to
    -        # model.persist_switch_by_default (defaults to True so /model survives
    -        # across sessions).
    -        persist_global = resolve_persist_behavior(is_global_flag, is_session, is_once=one_turn)
    +        # Resolve the effective persistence once: --global forces persist,
    +        # --session/--once force session-scope, otherwise defer to
    +        # model.persist_switch_by_default (defaults to False so /model is
    +        # session-scoped unless the user opts in).
    +        persist_global = resolve_persist_behavior(
    +            is_global_flag, is_session, is_once=one_turn,
    +            explicit_provider=explicit_provider,
    +        )
     
             # --refresh: wipe the on-disk picker cache before building the
             # provider list. Forces a live re-fetch of every authed provider's
    ```

    ```diff
    @@ -13157,8 +13164,8 @@ def handle_enter(event):
                 # --- /model picker modal ---
                 if self._model_picker_state:
                     try:
    -                    # Picker selections persist by default (same default as
    -                    # /model <name>); honour model.persist_switch_by_default.
    +                    # Picker selections follow the same session-scoped default
    +                    # as /model <name>; honour model.persist_switch_by_default.
                         from hermes_cli.model_switch import resolve_persist_behavior
     
                         self._handle_model_picker_selection(
    ```
  - `gateway/slash_commands.py`

    ```diff
    @@ -1437,10 +1437,10 @@ async def _handle_model_command(self, event: MessageEvent) -> Optional[str]:
     
             Supports:
               /model                              — interactive picker (Telegram/Discord) or text list
    -          /model <name>                       — switch model (persists by default)
    +          /model <name>                       — switch model (this session only)
               /model <name> --once                — switch for the next turn only
    -          /model <name> --session             — switch for this session only
    -          /model <name> --global              — switch and persist (explicit)
    +          /model <name> --session             — switch for this session only (explicit)
    +          /model <name> --global              — switch and persist to config.yaml
               /model <name> --provider <provider> — switch provider + model
               /model --provider <provider>        — switch to provider, auto-detect model
             """
    ```

    ```diff
    @@ -1478,6 +1478,7 @@ async def _handle_model_command(self, event: MessageEvent) -> Optional[str]:
                 is_global_flag,
                 is_session,
                 is_once=one_turn,
    +            explicit_provider=explicit_provider,
             )
     
             # --refresh: bust the disk cache so the picker shows live data.
    ```
  - `hermes_cli/cli_commands_mixin.py`

    ```diff
    @@ -2515,13 +2515,14 @@ def _handle_reasoning_command(self, cmd: str):
     
             Usage:
                 /reasoning              Show current effort level and display state
    -            /reasoning <level>      Set effort (none, minimal, low, medium, high, xhigh, max, ultra)
    +            /reasoning <level>      Set effort for this session only (none, minimal, low, medium, high, xhigh, max, ultra)
    +            /reasoning <level> --global  Persist reasoning effort to config.yaml
                 /reasoning show|on      Show model thinking/reasoning in output
                 /reasoning hide|off     Hide model thinking/reasoning from output
                 /reasoning full         Show complete thinking (no 10-line clamp)
                 /reasoning clamp        Collapse long thinking to the first 10 lines
             """
    -        from cli import _ACCENT, _DIM, _RST, _cprint, _parse_reasoning_config, save_config_value
    +        from cli import CLI_CONFIG, _ACCENT, _DIM, _RST, _cprint, _parse_reasoning_config, save_config_value
             parts = cmd.strip().split(maxsplit=1)
     
             if len(parts) < 2:
    ```

    ```diff
    @@ -2537,10 +2538,20 @@ def _handle_reasoning_command(self, cmd: str):
                 full_state = "full" if getattr(self, "reasoning_full", False) else "clamped to 10 lines"
                 _cprint(f"  {_ACCENT}Reasoning effort:  {level}{_RST}")
                 _cprint(f"  {_ACCENT}Reasoning display: {display_state} ({full_state}){_RST}")
    -            _cprint(f"  {_DIM}Usage: /reasoning <none|minimal|low|medium|high|xhigh|max|ultra|show|hide|full|clamp>{_RST}")
    +            _cprint(f"  {_DIM}Usage: /reasoning <none|minimal|low|medium|high|xhigh|max|ultra|show|hide|full|clamp> [--global]{_RST}")
                 return
     
             arg = parts[1].strip().lower()
    +        arg_tokens = arg.split()
    +        # Session scope is the default; --global opts into persisting to
    +        # config.yaml. --session is accepted as an explicit no-op for parity
    +        # with /model and the gateway /reasoning handler.
    +        explicit_global = "--global" in arg_tokens
    +        if explicit_global or "--session" in arg_tokens:
    +            arg = " ".join(
    +                token for token in arg_tokens
    +                if token not in ("--global", "--session")
    +            )
     
             # Display toggle
             if arg in {"show", "on"}:
    ```

    ```diff
    @@ -2580,15 +2591,23 @@ def _handle_reasoning_command(self, cmd: str):
                 _cprint(f"  {_DIM}(._.) Unknown argument: {arg}{_RST}")
                 _cprint(f"  {_DIM}Valid levels: none, minimal, low, medium, high, xhigh, max, ultra{_RST}")
                 _cprint(f"  {_DIM}Display:      show, hide{_RST}")
    +            _cprint(f"  {_DIM}Scope:        session-scoped by default, --global to persist{_RST}")
                 return
     
             self.reasoning_config = parsed
             self.agent = None  # Force agent re-init with new reasoning config
     
    -        if save_config_value("agent.reasoning_effort", arg):
    +        if explicit_global and save_config_value("agent.reasoning_effort", arg):
    +            agent_cfg = CLI_CONFIG.get("agent")
    +            if not isinstance(agent_cfg, dict):
    +                agent_cfg = {}
    +                CLI_CONFIG["agent"] = agent_cfg
    +            agent_cfg["reasoning_effort"] = arg
                 _cprint(f"  {_ACCENT}✓ Reasoning effort set to '{arg}' (saved to config){_RST}")
    +        elif explicit_global:
    +            _cprint(f"  {_ACCENT}✓ Reasoning effort set to '{arg}' (session only; config save failed){_RST}")
             else:
    -            _cprint(f"  {_ACCENT}✓ Reasoning effort set to '{arg}' (session only){_RST}")
    +            _cprint(f"  {_ACCENT}✓ Reasoning effort set to '{arg}' (this session — use --global to persist){_RST}")
     
         def _handle_busy_command(self, cmd: str):
             """Handle /busy — control what Enter does while Hermes is working.
    ```
  - `hermes_cli/commands.py`

    ```diff
    @@ -131,7 +131,7 @@ class CommandDef:
         # Configuration
         CommandDef("config", "Show current configuration", "Configuration",
                    cli_only=True),
    -    CommandDef("model", "Switch model (persists by default)", "Configuration",
    +    CommandDef("model", "Switch model (session-scoped; --global to persist)", "Configuration",
                    args_hint="[model] [--provider name] [--global|--session] [--refresh]"),
         CommandDef("codex-runtime", "Toggle codex app-server runtime for OpenAI/Codex models",
                    "Configuration", aliases=("codex_runtime",),
    ```

    ```diff
    @@ -153,8 +153,8 @@ class CommandDef:
         CommandDef("yolo", "Toggle YOLO mode (skip all dangerous command approvals)",
                    "Configuration"),
         CommandDef("reasoning", "Manage reasoning effort and display", "Configuration",
    -               args_hint="[level|show|hide|full|clamp]",
    -               subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "show", "hide", "on", "off", "full", "clamp")),
    +               args_hint="[level|show|hide|full|clamp] [--global]",
    +               subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "show", "hide", "on", "off", "full", "clamp", "--global")),
         CommandDef("fast", "Toggle fast mode — OpenAI Priority Processing / Anthropic Fast Mode (Normal/Fast)", "Configuration",
                    args_hint="[normal|fast|status]",
                    subcommands=("normal", "fast", "status", "on", "off")),
    ```
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -552,7 +552,12 @@ def parse_model_flags(raw_args: str) -> tuple[str, str, bool, bool, bool]:
         )
     
     
    -def resolve_persist_behavior(is_global: bool, is_session: bool, is_once: bool = False) -> bool:
    +def resolve_persist_behavior(
    +    is_global: bool,
    +    is_session: bool,
    +    is_once: bool = False,
    +    explicit_provider: str = "",
    +) -> bool:
         """Decide whether a ``/model`` switch should persist to ``config.yaml``.
     
         Resolution order:
    ```

    ```diff
    @@ -560,13 +565,19 @@ def resolve_persist_behavior(is_global: bool, is_session: bool, is_once: bool =
         1. ``--once`` explicitly opts out → ``False`` (next turn only).
         2. ``--session`` explicitly opts out → ``False`` (this session only).
         3. ``--global`` explicitly opts in → ``True``.
    -    4. Otherwise defer to ``model.persist_switch_by_default`` in
    -       ``config.yaml`` (defaults to ``True``, so a plain ``/model <name>``
    -       survives across sessions — the behavior users expect).
    +    4. ``--provider`` given without an explicit persist flag → ``False``
    +       (session only).  Provider switches are typically exploratory — the
    +       user is trying a different backend for this conversation, not
    +       reconfiguring the default.  ``--global`` can still force persist.
    +    5. Otherwise defer to ``model.persist_switch_by_default`` in
    +       ``config.yaml`` (defaults to ``False``: a plain ``/model <name>``
    +       affects only the current session).  Users who want the old
    +       persist-by-default behavior can set the key to ``true``; a one-off
    +       ``--global`` always persists.
     
         The config read is defensive: on a fresh install ``model`` may be a
         flat string rather than a dict, in which case the built-in default
    -    (``True``) applies.
    +    (``False``) applies.
         """
         if is_once:
             return False
    ```

    ```diff
    @@ -574,15 +585,17 @@ def resolve_persist_behavior(is_global: bool, is_session: bool, is_once: bool =
             return False
         if is_global:
             return True
    +    if explicit_provider:
    +        return False
         try:
             from hermes_cli.config import load_config
     
             model_cfg = load_config().get("model")
             if isinstance(model_cfg, dict):
    -            return bool(model_cfg.get("persist_switch_by_default", True))
    +            return bool(model_cfg.get("persist_switch_by_default", False))
         except Exception:
             pass
    -    return True
    +    return False
     
     
     # ---------------------------------------------------------------------------
    ```
  - `tests/cli/test_reasoning_command.py`

    ```diff
    @@ -154,6 +154,72 @@ def test_status_with_explicit_level(self):
             level = rc.get("effort", "medium")
             self.assertEqual(level, "xhigh")
     
    +    def test_effort_defaults_to_session_only(self):
    +        """Plain /reasoning <level> is session-scoped — no config write."""
    +        from hermes_cli.cli_commands_mixin import CLICommandsMixin
    +
    +        stub = self._make_cli(reasoning_config={"enabled": True, "effort": "medium"})
    +        with patch("cli.save_config_value") as save_config, patch("cli._cprint"):
    +            CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high")
    +
    +        save_config.assert_not_called()
    +        self.assertEqual(stub.reasoning_config, {"enabled": True, "effort": "high"})
    +        self.assertIsNone(stub.agent)
    +
    +    def test_effort_global_flag_persists_config(self):
    +        """--global opts into persisting the effort to config.yaml."""
    +        from cli import CLI_CONFIG
    +        from hermes_cli.cli_commands_mixin import CLICommandsMixin
    +
    +        stub = self._make_cli(reasoning_config={"enabled": True, "effort": "medium"})
    +        with patch.dict(CLI_CONFIG.setdefault("agent", {}), {"reasoning_effort": "medium"}), \
    +             patch("cli.save_config_value", return_value=True) as save_config, \
    +             patch("cli._cprint"):
    +            CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high --global")
    +            self.assertEqual(CLI_CONFIG["agent"]["reasoning_effort"], "high")
    +
    +        save_config.assert_called_once_with("agent.reasoning_effort", "high")
    +        self.assertEqual(stub.reasoning_config, {"enabled": True, "effort": "high"})
    +        self.assertIsNone(stub.agent)
    +
    +    def test_effort_session_flag_does_not_persist_config(self):
    +        """--session (explicit no-op alias for the default) stays session-only."""
    +        from hermes_cli.cli_commands_mixin import CLICommandsMixin
    +
    +        stub = self._make_cli(reasoning_config={"enabled": True, "effort": "medium"})
    +        with patch("cli.save_config_value") as save_config, patch("cli._cprint"):
    +            CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high --session")
    +
    +        save_config.assert_not_called()
    +        self.assertEqual(stub.reasoning_config, {"enabled": True, "effort": "high"})
    +        self.assertIsNone(stub.agent)
    +
    +    def test_new_session_clears_session_reasoning_override(self):
    +        """/new and /clear must not carry a session-only effort override forward."""
    +        from cli import CLI_CONFIG, HermesCLI
    +
    +        agent = SimpleNamespace(
    +            reasoning_config={"enabled": True, "effort": "high"},
    +            reset_session_state=MagicMock(),
    +        )
    +        stub = SimpleNamespace(
    +            agent=agent,
    +            conversation_history=[],
    +            session_id="old-session",
    +            _session_db=None,
    +            _pending_title=None,
    +            _resumed=False,
    +            reasoning_config={"enabled": True, "effort": "high"},
    +            _notify_session_boundary=MagicMock(),
    +        )
    +
    +        with patch.dict(CLI_CONFIG.setdefault("agent", {}), {"reasoning_effort": "medium"}):
    +            HermesCLI.new_session(stub, silent=True)
    +
    +        self.assertEqual(stub.reasoning_config, {"enabled": True, "effort": "medium"})
    +        self.assertEqual(agent.reasoning_config, {"enabled": True, "effort": "medium"})
    +        agent.reset_session_state.assert_called_once()
    +
     
     # ---------------------------------------------------------------------------
     # Reasoning extraction and result dict
    ```
  - `tests/gateway/test_25107_stale_base_url_api_mode.py`

    ```diff
    @@ -186,7 +186,7 @@ async def test_picker_tap_to_custom_clears_stale_base_url_and_api_mode(tmp_path,
         adapter = _FakePickerAdapter()
         cfg_path = _setup_isolated_home(tmp_path, monkeypatch, dict(_STALE_MODEL_CFG))
     
    -    confirmation = await _drive_picker(_make_runner(adapter), _make_event("/model"))
    +    confirmation = await _drive_picker(_make_runner(adapter), _make_event("/model --global"))
     
         assert confirmation is not None
         written = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
    ```
  - `tests/gateway/test_model_command_flat_string_config.py`

    ```diff
    @@ -159,11 +159,11 @@ async def test_model_global_persists_when_config_has_proper_dict_model(tmp_path,
     
     
     @pytest.mark.asyncio
    -async def test_model_no_flag_persists_by_default(tmp_path, monkeypatch):
    -    """A plain ``/model X`` (no --global) now persists to config.yaml.
    +async def test_model_no_flag_is_session_scoped_by_default(tmp_path, monkeypatch):
    +    """A plain ``/model X`` (no --global) does NOT persist to config.yaml.
     
    -    This is the user-facing fix: switching models in one session survives
    -    into the next without re-typing the switch every time.
    +    This is the user-facing fix: switches are session-scoped unless the user
    +    opts in with ``--global`` or sets ``model.persist_switch_by_default: true``.
         """
         cfg_path = _setup_isolated_home(
             tmp_path,
    ```

    ```diff
    @@ -178,7 +178,7 @@ async def test_model_no_flag_persists_by_default(tmp_path, monkeypatch):
         assert result is not None
         assert "gpt-5.5" in result
         written = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
    -    assert written["model"]["default"] == "gpt-5.5"
    +    assert written["model"]["default"] == "old-model"
     
     
     @pytest.mark.asyncio
    ```
  - `tests/gateway/test_model_picker_persist.py`

    ```diff
    @@ -8,9 +8,10 @@
     *typing* the same model persisted — a contradiction the same PR introduced.
     
     After the fix (#49176), the picker callback honors the resolved
    -``persist_global`` (defaults to ``True``, still respects ``--session``) and runs
    -the same read-modify-write block the text path uses, so a tapped model survives
    -across sessions like a typed one.
    +``persist_global`` and runs the same read-modify-write block the text path
    +uses, so a tapped model behaves exactly like a typed one.  Since the
    +session-scope-by-default change, both default to session-only and persist
    +only with ``--global`` (or ``model.persist_switch_by_default: true``).
     
     These tests drive the real ``_handle_model_command`` with a fake picker-capable
     adapter that captures the ``on_model_selected`` callback, then invoke that
    ```

    ```diff
    @@ -176,14 +177,14 @@ async def _drive_picker(runner, event):
         ],
         ids=["nested-dict", "flat-string"],
     )
    -async def test_picker_tap_persists_by_default(tmp_path, monkeypatch, seed_model):
    -    """Tapping a model in the picker (bare /model) persists to config.yaml,
    -    matching the typed ``/model`` default — this is the #49176 fix. The written
    -    ``model:`` must always end up a nested dict regardless of the seed shape."""
    +async def test_picker_tap_global_flag_persists(tmp_path, monkeypatch, seed_model):
    +    """Tapping a model in a ``/model --global`` picker persists to config.yaml,
    +    matching the typed ``/model --global`` path. The written ``model:`` must
    +    always end up a nested dict regardless of the seed shape."""
         adapter = _FakePickerAdapter()
         cfg_path = _setup_isolated_home(tmp_path, monkeypatch, seed_model)
     
    -    confirmation = await _drive_picker(_make_runner(adapter), _make_event("/model"))
    +    confirmation = await _drive_picker(_make_runner(adapter), _make_event("/model --global"))
     
         assert confirmation is not None
         assert "gpt-5.5" in confirmation
    ```

    ```diff
    @@ -198,6 +199,34 @@ async def test_picker_tap_persists_by_default(tmp_path, monkeypatch, seed_model)
         assert "api_mode" not in written["model"]
     
     
    [redacted:email]
    +async def test_picker_tap_is_session_scoped_by_default(tmp_path, monkeypatch):
    +    """Tapping a model in a bare ``/model`` picker applies an in-memory session
    +    override and does NOT touch config.yaml — switches are session-scoped
    +    unless the user opts in with ``--global`` (or sets
    +    ``model.persist_switch_by_default: true``)."""
    +    adapter = _FakePickerAdapter()
    +    cfg_path = _setup_isolated_home(
    +        tmp_path, monkeypatch, {"default": "old-model", "provider": "openrouter"}
    +    )
    +    runner = _make_runner(adapter)
    +
    +    confirmation = await _drive_picker(runner, _make_event("/model"))
    +
    +    assert confirmation is not None
    +    assert "gpt-5.5" in confirmation
    +    # The session override IS applied in-memory (the switch worked).
    +    assert runner._session_model_overrides, "session override should be set"
    +    assert any(
    +        ov.get("model") == "gpt-5.5"
    +        for ov in runner._session_model_overrides.values()
    +    )
    +    # But config.yaml is untouched — session-scoped by default.
    +    written = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
    +    assert written["model"]["default"] == "old-model"
    +    assert written["model"]["provider"] == "openrouter"
    +
    +
     @pytest.mark.asyncio
     async def test_picker_tap_session_flag_does_not_persist(tmp_path, monkeypatch):
         """``/model --session`` then a picker tap stays in-memory only — config
    ```
  - `tests/hermes_cli/test_model_switch_persist_default.py`

    ```diff
    @@ -1,11 +1,12 @@
    -"""Tests for persist-by-default model switching.
    +"""Tests for session-scoped-by-default model switching.
     
     Covers:
     - ``parse_model_flags`` recognises ``--session`` (and keeps ``--global``).
     - ``resolve_persist_behavior`` applies the config-gated default and the
       ``--session`` / ``--global`` overrides.
    -- The default (no flags) persists, which is the user-facing fix: a plain
    -  ``/model <name>`` survives across sessions.
    +- The default (no flags) is session-only, which is the user-facing fix: a
    +  plain ``/model <name>`` affects only the current session unless the user
    +  passes ``--global`` or sets ``model.persist_switch_by_default: true``.
     """
     
     from unittest.mock import patch
    ```

    ```diff
    @@ -73,10 +74,10 @@ def test_global_flag_always_persists(self):
             with _config({"model": {"persist_switch_by_default": False}}):
                 assert resolve_persist_behavior(True, False) is True
     
    -    def test_default_persists_when_config_missing(self):
    -        # No model section at all → built-in default (True).
    +    def test_default_session_only_when_config_missing(self):
    +        # No model section at all → built-in default (False, session-only).
             with _config({}):
    -            assert resolve_persist_behavior(False, False) is True
    +            assert resolve_persist_behavior(False, False) is False
     
         def test_default_persists_when_key_true(self):
             with _config({"model": {"persist_switch_by_default": True}}):
    ```

    ```diff
    @@ -87,15 +88,35 @@ def test_default_session_only_when_key_false(self):
                 assert resolve_persist_behavior(False, False) is False
     
         def test_default_when_model_is_flat_string(self):
    -        # Fresh install: ``model: ""`` (not a dict) → built-in default True.
    +        # Fresh install: ``model: ""`` (not a dict) → built-in default False.
             with _config({"model": ""}):
    -            assert resolve_persist_behavior(False, False) is True
    +            assert resolve_persist_behavior(False, False) is False
     
         def test_session_overrides_global_when_both_set(self):
             # --session is the explicit opt-out and wins over --global.
             with _config({"model": {"persist_switch_by_default": True}}):
                 assert resolve_persist_behavior(True, True) is False
     
    +    def test_provider_flag_defaults_to_session_only(self):
    +        # --provider without --global/--session → session only.
    +        with _config({"model": {"persist_switch_by_default": True}}):
    +            assert resolve_persist_behavior(False, False, explicit_provider="anthropic") is False
    +
    +    def test_provider_with_global_still_persists(self):
    +        # --provider + --global → persists.
    +        with _config({"model": {"persist_switch_by_default": False}}):
    +            assert resolve_persist_behavior(True, False, explicit_provider="anthropic") is True
    +
    +    def test_provider_with_session_still_session_only(self):
    +        # --provider + --session → session only.
    +        with _config({"model": {"persist_switch_by_default": True}}):
    +            assert resolve_persist_behavior(False, True, explicit_provider="anthropic") is False
    +
    +    def test_no_provider_uses_config_default(self):
    +        # No --provider → respects config default (True).
    +        with _config({"model": {"persist_switch_by_default": True}}):
    +            assert resolve_persist_behavior(False, False, explicit_provider="") is True
    +
     
     # ---------------------------------------------------------------------------
     # helper
    ```
  - `tests/hermes_cli/test_reasoning_full_command.py`

    ```diff
    @@ -55,7 +55,7 @@ def test_reasoning_full_sets_and_persists(tmp_path, monkeypatch):
         assert saved["display"]["reasoning_full"] is True
     
     
    -def test_reasoning_clamp_resets_and_persists(tmp_path, monkeypatch):
    +def test_reasoning_clamp_resets_and_persists(tmp_path, monkeypatch, capsys):
         hh = _seed_config(tmp_path, monkeypatch)
         s = _Stub()
         s.reasoning_full = True
    ```

    ```diff
    @@ -64,6 +64,7 @@ def test_reasoning_clamp_resets_and_persists(tmp_path, monkeypatch):
         assert s.reasoning_full is False
         saved = yaml.safe_load((hh / "config.yaml").read_text())
         assert saved["display"]["reasoning_full"] is False
    +    assert "Unknown argument" not in capsys.readouterr().out
     
     
     def test_reasoning_all_is_alias_for_full(tmp_path, monkeypatch):
    ```
  - `tests/test_tui_gateway_server.py`

    ```diff
    @@ -4649,8 +4649,18 @@ def test_complete_slash_details_args():
         assert any(item["text"] == "expanded" for item in resp_mode["result"]["items"])
     
     
    +def test_complete_slash_reasoning_includes_current_efforts_and_global_scope():
    +    resp = server.handle_request(
    +        {"id": "1", "method": "complete.slash", "params": {"text": "/reasoning "}}
    +    )
    +
    +    values = {item["text"] for item in resp["result"]["items"]}
    +    assert {"max", "ultra", "--global"} <= values
    +
    +
     def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypatch):
         monkeypatch.setattr(server, "_hermes_home", tmp_path)
    +    (tmp_path / "config.yaml").write_text("agent:\n  reasoning_effort: medium\n", encoding="utf-8")
         agent = types.SimpleNamespace(reasoning_config=None)
         server._sessions["sid"] = _session(agent=agent)
     
    ```

    ```diff
    @@ -4658,11 +4668,42 @@ def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypat
             {
                 "id": "1",
                 "method": "config.set",
    -            "params": {"session_id": "sid", "key": "reasoning", "value": "low"},
    +            "params": {
    +                "session_id": "sid",
    +                "key": "reasoning",
    +                "value": "low",
    +            },
             }
         )
         assert resp_effort["result"]["value"] == "low"
         assert agent.reasoning_config == {"enabled": True, "effort": "low"}
    +    assert server._sessions["sid"]["create_reasoning_override"] == {"enabled": True, "effort": "low"}
    +    assert server._load_cfg()["agent"]["reasoning_effort"] == "medium"
    +
    +    resp_status = server.handle_request(
    +        {
    +            "id": "5",
    +            "method": "config.get",
    +            "params": {"session_id": "sid", "key": "reasoning"},
    +        }
    +    )
    +    assert resp_status["result"]["value"] == "low"
    +
    +    resp_global_status = server.handle_request(
    +        {"id": "6", "method": "config.get", "params": {"key": "reasoning"}}
    +    )
    +    assert resp_global_status["result"]["value"] == "medium"
    +
    +    del server._sessions["sid"]["create_reasoning_override"]
    +    agent.reasoning_config = {"enabled": True, "effort": "high"}
    +    resp_agent_status = server.handle_request(
    +        {
    +            "id": "7",
    +            "method": "config.get",
    +            "params": {"session_id": "sid", "key": "reasoning"},
    +        }
    +    )
    +    assert resp_agent_status["result"]["value"] == "high"
     
         resp_show = server.handle_request(
             {
    ```

    ```diff
    @@ -4714,6 +4755,36 @@ def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypat
         assert cfg_clamp["display"]["sections"]["thinking"] == "collapsed"
     
     
    +def test_config_set_reasoning_global_scope_clears_session_override(tmp_path, monkeypatch):
    +    monkeypatch.setattr(server, "_hermes_home", tmp_path)
    +    (tmp_path / "config.yaml").write_text("agent:\n  reasoning_effort: medium\n", encoding="utf-8")
    +    agent = types.SimpleNamespace(reasoning_config=None)
    +    server._sessions["sid"] = _session(agent=agent)
    +    server._sessions["sid"]["create_reasoning_override"] = {"enabled": True, "effort": "low"}
    +
    +    resp = server.handle_request(
    +        {
    +            "id": "1",
    +            "method": "config.set",
    +            "params": {
    +                "session_id": "sid",
    +                "key": "reasoning",
    +                "value": "high",
    +                "scope": "global",
    +            },
    +        }
    +    )
    +
    +    assert resp["result"]["value"] == "high"
    +    assert server._load_cfg()["agent"]["reasoning_effort"] == "high"
    +    assert "create_reasoning_override" not in server._sessions["sid"]
    +
    +    status = server.handle_request(
    +        {"id": "2", "method": "config.get", "params": {"session_id": "sid", "key": "reasoning"}}
    +    )
    +    assert status["result"]["value"] == "high"
    +
    +
     def test_config_set_verbose_updates_session_mode_and_agent(tmp_path, monkeypatch):
         monkeypatch.setattr(server, "_hermes_home", tmp_path)
         agent = types.SimpleNamespace(verbose_logging=False)
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -3221,6 +3221,7 @@ def _apply_model_switch(
                 is_global_flag,
                 is_session,
                 is_once=one_turn,
    +            explicit_provider=explicit_provider,
             )
         )
         if not model_input:
    ```

    ```diff
    @@ -11554,6 +11555,8 @@ def _resolve_toggle(current: bool) -> bool:
                 from hermes_constants import parse_reasoning_effort
     
                 arg = str(value or "").strip().lower()
    +            scope = str(params.get("scope") or "").strip().lower()
    +            global_scope = scope == "global"
                 if arg in {"show", "on"}:
                     cfg = _load_cfg()
                     display = (
    ```

    ```diff
    @@ -11633,23 +11636,25 @@ def _resolve_toggle(current: bool) -> bool:
                 parsed = parse_reasoning_effort(arg)
                 if parsed is None:
                     return _err(rid, 4002, f"unknown reasoning value: {value}")
    -            if session is not None:
    +            if global_scope or session is None:
    +                _write_config_key("agent.reasoning_effort", arg)
    +                if session is not None:
    +                    session.pop("create_reasoning_override", None)
    +            else:
                     # Session-scoped, like the messaging gateway's `/reasoning
                     # <level>` (global persistence is `--global` / Settings →
                     # Model territory). Writing config.yaml here let every
                     # desktop model-menu selection rewrite the user's global
                     # agent.reasoning_effort to the preset default.
                     session["create_reasoning_override"] = parsed
    -                if session.get("agent") is not None:
    -                    session["agent"].reasoning_config = parsed
    -                    _persist_live_session_runtime(session)
    -                    _emit(
    -                        "session.info",
    -                        params.get("session_id", ""),
    -                        _session_info(session["agent"], session),
    -                    )
    -            else:
    -                _write_config_key("agent.reasoning_effort", arg)
    +            if session and session.get("agent") is not None:
    +                session["agent"].reasoning_config = parsed
    +                _persist_live_session_runtime(session)
    +                _emit(
    +                    "session.info",
    +                    params.get("session_id", ""),
    +                    _session_info(session["agent"], session),
    +                )
                 return _ok(rid, {"key": key, "value": arg})
             except Exception as e:
                 return _err(rid, 5001, str(e))
    ```

    ```diff
    @@ -12344,19 +12349,23 @@ def _(rid, params: dict) -> dict:
             )
         if key == "reasoning":
             cfg = _load_cfg()
    -        effort = ""
    -        # Prefer the session's live value — `config.set reasoning` is
    -        # session-scoped, so the global key may not reflect this chat.
             session = _sessions.get(params.get("session_id", ""))
    -        live = getattr((session or {}).get("agent"), "reasoning_config", None)
    -        if live is None and session is not None:
    -            live = session.get("create_reasoning_override")
    -        if isinstance(live, dict):
    -            if live.get("enabled") is False:
    +        reasoning_config = None
    +        if session is not None:
    +            if isinstance(session.get("create_reasoning_override"), dict):
    +                reasoning_config = session.get("create_reasoning_override")
    +            else:
    +                agent = session.get("agent")
    +                agent_reasoning = getattr(agent, "reasoning_config", None)
    +                if isinstance(agent_reasoning, dict):
    +                    reasoning_config = agent_reasoning
    +
    +        if isinstance(reasoning_config, dict):
    +            if reasoning_config.get("enabled") is False:
                     effort = "none"
                 else:
    -                effort = str(live.get("effort", "") or "")
    -        if not effort:
    +                effort = str(reasoning_config.get("effort") or "medium")
    +        else:
                 raw_effort = (cfg.get("agent") or {}).get("reasoning_effort", "")
                 if raw_effort is False:
                     # YAML `reasoning_effort: false`/`off`/`no` — thinking
    ```
  - `ui-tui/src/__tests__/createSlashHandler.test.ts`

    ```diff
    @@ -267,6 +267,55 @@ describe('createSlashHandler', () => {
         })
       })
     
    +  it('reads /reasoning status for the active session', () => {
    +    patchUiState({ sid: 'sid-abc' })
    +    const ctx = buildCtx()
    +
    +    expect(createSlashHandler(ctx)('/reasoning')).toBe(true)
    +    expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.get', {
    +      key: 'reasoning',
    +      session_id: 'sid-abc'
    +    })
    +  })
    +
    +  it.each(['low', 'max', 'ultra'])('sends plain /reasoning %s without a scope (session default)', effort => {
    +    patchUiState({ sid: 'sid-abc' })
    +    const ctx = buildCtx()
    +
    +    expect(createSlashHandler(ctx)(`/reasoning ${effort}`)).toBe(true)
    +    expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', {
    +      key: 'reasoning',
    +      session_id: 'sid-abc',
    +      value: effort
    +    })
    +  })
    +
    +  it('sends /reasoning <level> --global as global config.set', () => {
    +    patchUiState({ sid: 'sid-abc' })
    +    const ctx = buildCtx()
    +
    +    expect(createSlashHandler(ctx)('/reasoning high --global')).toBe(true)
    +    expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', {
    +      key: 'reasoning',
    +      scope: 'global',
    +      session_id: 'sid-abc',
    +      value: 'high'
    +    })
    +  })
    +
    +  it('strips /reasoning session flags before config.set', () => {
    +    patchUiState({ sid: 'sid-abc' })
    +    const ctx = buildCtx()
    +
    +    expect(createSlashHandler(ctx)('/reasoning low --session')).toBe(true)
    +    expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', {
    +      key: 'reasoning',
    +      scope: 'session',
    +      session_id: 'sid-abc',
    +      value: 'low'
    +    })
    +  })
    +
       it('opens the skills hub locally for bare /skills', () => {
         const ctx = buildCtx()
     
    ```
  - `ui-tui/src/app/slash/commands/session.ts`

    ```diff
    @@ -23,6 +23,8 @@ import type { SlashCommand } from '../types.js'
     const USAGE_CTA = 'Run /subscription to change plan · /topup to add to your balance'
     
     const TUI_SESSION_MODEL_RE = new RegExp(`(?:^|\\s)${TUI_SESSION_MODEL_FLAG}(?:\\s|$)`)
    +const REASONING_SESSION_FLAGS = new Set(['--session'])
    +const REASONING_GLOBAL_FLAGS = new Set(['--global'])
     
     const modelValueForConfigSet = (arg: string) => {
       const trimmed = arg.trim()
    ```

    ```diff
    @@ -38,6 +40,37 @@ const modelValueForConfigSet = (arg: string) => {
       return trimmed
     }
     
    +const reasoningConfigPayload = (arg: string, sid: string) => {
    +  const parts = arg.trim().split(/\s+/).filter(Boolean)
    +  let scope = ''
    +  const valueParts: string[] = []
    +
    +  for (const part of parts) {
    +    const flag = part.toLowerCase()
    +    if (REASONING_GLOBAL_FLAGS.has(flag)) {
    +      scope = 'global'
    +      continue
    +    }
    +    if (REASONING_SESSION_FLAGS.has(flag)) {
    +      // Session scope is the default; accept the flag for parity with /model.
    +      if (!scope) {
    +        scope = 'session'
    +      }
    +      continue
    +    }
    +    valueParts.push(part)
    +  }
    +
    +  const value = valueParts.join(' ')
    +
    +  return {
    +    key: 'reasoning',
    +    session_id: sid,
    +    value,
    +    ...(scope ? { scope } : {})
    +  }
    +}
    +
     export const sessionCommands: SlashCommand[] = [
       {
         aliases: ['bg', 'btw'],
    ```

    ```diff
    @@ -445,7 +478,7 @@ export const sessionCommands: SlashCommand[] = [
         run: (arg, ctx) => {
           if (!arg) {
             return ctx.gateway
    -          .rpc<ConfigGetValueResponse>('config.get', { key: 'reasoning' })
    +          .rpc<ConfigGetValueResponse>('config.get', { key: 'reasoning', session_id: ctx.sid })
               .then(
                 ctx.guarded<ConfigGetValueResponse>(
                   r => r.value && ctx.transcript.sys(`reasoning: ${r.value} · display ${r.display || 'hide'}`)
    ```

    ```diff
    @@ -453,29 +486,31 @@ export const sessionCommands: SlashCommand[] = [
               )
           }
     
    -      ctx.gateway.rpc<ConfigSetResponse>('config.set', { key: 'reasoning', session_id: ctx.sid, value: arg }).then(
    -        ctx.guarded<ConfigSetResponse>(r => {
    -          if (!r.value) {
    -            return
    -          }
    +      ctx.gateway
    +        .rpc<ConfigSetResponse>('config.set', reasoningConfigPayload(arg, ctx.sid ?? ''))
    +        .then(
    +          ctx.guarded<ConfigSetResponse>(r => {
    +            if (!r.value) {
    +              return
    +            }
     
    -          if (r.value === 'hide') {
    -            patchUiState(state => ({
    -              ...state,
    -              sections: { ...state.sections, thinking: 'hidden' },
    -              showReasoning: false
    -            }))
    -          } else if (r.value === 'show') {
    -            patchUiState(state => ({
    -              ...state,
    -              sections: { ...state.sections, thinking: 'expanded' },
    -              showReasoning: true
    -            }))
    -          }
    +            if (r.value === 'hide') {
    +              patchUiState(state => ({
    +                ...state,
    +                sections: { ...state.sections, thinking: 'hidden' },
    +                showReasoning: false
    +              }))
    +            } else if (r.value === 'show') {
    +              patchUiState(state => ({
    +                ...state,
    +                sections: { ...state.sections, thinking: 'expanded' },
    +                showReasoning: true
    +              }))
    +            }
     
    -          ctx.transcript.sys(`reasoning: ${r.value}`)
    -        })
    -      )
    +            ctx.transcript.sys(`reasoning: ${r.value}`)
    +          })
    +        )
         }
       },
     
    ```
  - `website/docs/reference/cli-commands.md`

    ```diff
    @@ -199,7 +199,7 @@ Switch between already-configured models without leaving a session:
     /model openrouter:anthropic/claude-sonnet-4  # Switch back to cloud
     ```
     
    -By default, `/model` changes apply **to the current session only**. Add `--global` to persist the change to `config.yaml`:
    +By default, `/model` changes apply **to the current session only**. Add `--global` to persist the change to `config.yaml` (or set `model.persist_switch_by_default: true` to make every switch persist):
     
     ```
     /model claude-sonnet-4 --global     # Switch and save as new default
    ```

    ```diff
    @@ -209,7 +209,7 @@ By default, `/model` changes apply **to the current session only**. Add `--globa
     If you've only configured OpenRouter, `/model` will only show OpenRouter models. To add another provider (Anthropic, DeepSeek, Copilot, etc.), exit your session and run `hermes model` from the terminal.
     :::
     
    -Provider and base URL changes are persisted to `config.yaml` automatically. When switching away from a custom endpoint, the stale base URL is cleared to prevent it leaking into other providers.
    +On a `--global` switch, provider and base URL changes are persisted to `config.yaml` alongside the model. When switching away from a custom endpoint, the stale base URL is cleared to prevent it leaking into other providers.
     
     ## `hermes gateway`
     
    ```
  - `website/docs/user-guide/configuration.md`

    ```diff
    @@ -1323,13 +1323,17 @@ native Anthropic provider already controls effort directly and is unaffected.
     You can also change the reasoning effort at runtime with the `/reasoning` command:
     
     ```
    -/reasoning           # Show current effort level and display state
    -/reasoning high      # Set reasoning effort to high
    -/reasoning none      # Disable reasoning
    -/reasoning show      # Show model thinking above each response
    -/reasoning hide      # Hide model thinking
    +/reasoning                # Show current effort level and display state
    +/reasoning high           # Set reasoning effort to high (this session only)
    +/reasoning high --global  # Set effort and persist to config.yaml
    +/reasoning none           # Disable reasoning (this session only)
    +/reasoning show           # Show model thinking above each response
    +/reasoning hide           # Hide model thinking
     ```
     
    +Effort changes are session-scoped by default; add `--global` to save the
    +new level as your `agent.reasoning_effort` default.
    +
     #### Per-Model Reasoning Overrides
     
     You can set different reasoning effort levels for different models. This is useful when you want high reasoning for complex models but medium for faster ones:
    ```
- **#61488: fix(gateway): default /model to session scope on messaging platforms**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#67946`, `#58371`

## Fix status

- Merged to main in PR #67946 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-20`.
- Subsystem: `gateway`
