From dc0dbc938716555f7121fd9ca4d1783e3e80a585 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:25:38 -0700 Subject: [PATCH] fix(reasoning): default /reasoning to session scope in CLI and TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parity with the gateway /reasoning handler and the new /model default: a bare /reasoning now applies to the current session only; --global persists agent.reasoning_effort to config.yaml. --session is still accepted as an explicit alias for the default. Display toggles (show/hide/full/clamp) remain persistent as before — they are user preferences, not conversation state. Builds on YAMAGUCHI Seiji's #51158 (session-scope plumbing + /new reset, cherry-picked as the previous commit) with the default flipped to match the session-first policy. Fixes the CLI half of #54084. --- hermes_cli/cli_commands_mixin.py | 26 ++++++++++++------- hermes_cli/commands.py | 4 +-- tests/cli/test_reasoning_command.py | 20 +++++++++++--- tests/test_tui_gateway_server.py | 4 +-- .../src/__tests__/createSlashHandler.test.ts | 16 ++++++++++-- ui-tui/src/app/slash/commands/session.ts | 14 ++++++---- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index cd72f9842e2..c199c5ce649 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -2515,8 +2515,8 @@ class CLICommandsMixin: Usage: /reasoning Show current effort level and display state - /reasoning Set effort (none, minimal, low, medium, high, xhigh, max, ultra) - /reasoning --session Set reasoning effort for this session only + /reasoning Set effort for this session only (none, minimal, low, medium, high, xhigh, max, ultra) + /reasoning --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) @@ -2538,14 +2538,20 @@ class CLICommandsMixin: 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 [--session]{_RST}") + _cprint(f" {_DIM}Usage: /reasoning [--global]{_RST}") return arg = parts[1].strip().lower() arg_tokens = arg.split() - explicit_session = "--session" in arg_tokens - if explicit_session: - arg = " ".join(token for token in arg_tokens if token != "--session") + # 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"}: @@ -2585,23 +2591,23 @@ class CLICommandsMixin: _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: saved by default, --session for a temporary override{_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 not explicit_session and 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 not explicit_session: + 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. diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 073f56dd6d5..aa633f77099 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -153,8 +153,8 @@ COMMAND_REGISTRY: list[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] [--session]", - subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "show", "hide", "on", "off", "full", "clamp", "--session")), + 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")), diff --git a/tests/cli/test_reasoning_command.py b/tests/cli/test_reasoning_command.py index 1aff1af5908..868d8ff4f50 100644 --- a/tests/cli/test_reasoning_command.py +++ b/tests/cli/test_reasoning_command.py @@ -154,8 +154,20 @@ class TestHandleReasoningCommand(unittest.TestCase): level = rc.get("effort", "medium") self.assertEqual(level, "xhigh") - def test_effort_defaults_to_global_save(self): - """Plain /reasoning keeps the existing config-writing behavior.""" + def test_effort_defaults_to_session_only(self): + """Plain /reasoning 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 @@ -163,7 +175,7 @@ class TestHandleReasoningCommand(unittest.TestCase): 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") + 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") @@ -171,7 +183,7 @@ class TestHandleReasoningCommand(unittest.TestCase): self.assertIsNone(stub.agent) def test_effort_session_flag_does_not_persist_config(self): - """--session opts into a temporary session-only effort override.""" + """--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"}) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index beaf45d9cd1..fdffa98e1f7 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -4649,13 +4649,13 @@ 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_session_scope(): +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", "--session"} <= values + assert {"max", "ultra", "--global"} <= values def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypatch): diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 2f0c0c7e4dd..7096798ecf9 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -278,19 +278,31 @@ describe('createSlashHandler', () => { }) }) - it.each(['low', 'max', 'ultra'])('sends plain /reasoning %s as global config.set', effort => { + 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', - scope: 'global', session_id: 'sid-abc', value: effort }) }) + it('sends /reasoning --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() diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index caabea9e815..9eba0f67a16 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -24,7 +24,7 @@ const USAGE_CTA = 'Run /subscription to change plan · /topup to add to your bal const TUI_SESSION_MODEL_RE = new RegExp(`(?:^|\\s)${TUI_SESSION_MODEL_FLAG}(?:\\s|$)`) const REASONING_SESSION_FLAGS = new Set(['--session']) -const REASONING_EFFORT_VALUES = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']) +const REASONING_GLOBAL_FLAGS = new Set(['--global']) const modelValueForConfigSet = (arg: string) => { const trimmed = arg.trim() @@ -47,17 +47,21 @@ const reasoningConfigPayload = (arg: string, sid: 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)) { - scope = 'session' + // Session scope is the default; accept the flag for parity with /model. + if (!scope) { + scope = 'session' + } continue } valueParts.push(part) } const value = valueParts.join(' ') - if (!scope && REASONING_EFFORT_VALUES.has(value.toLowerCase())) { - scope = 'global' - } return { key: 'reasoning',