From 6c2a94f4b634dbbdce09e5ff26b88bcfbd845198 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 23 Sep 2026 04:43:48 -0700 Subject: [PATCH] fix(tui-gateway): stale session_id is refused once at the config.set seam for every session-scoped key Sweep of the #119983 shape: `reasoning` had the same stale-session -> global-write fall-through as `fast`/`yolo` (a reaped session_id wrote agent.reasoning_effort into config.yaml and answered success), and `model` already carried its own 4001. One check at the dispatcher replaces the three per-handler guards: a non-empty session_id this backend no longer holds, on a key whose sessionless branch writes a wider scope (config.yaml agent.*, the process env every later child inherits), answers 4001 so the client resumes. `scope="global"` is still honoured; a request with no session_id at all is still the sessionless path. Live (temp home, stale id): base wrote reasoning_effort: high and HERMES_YOLO_MODE=1; head leaves config.yaml and os.environ byte-identical and answers 4001 for all three. --- tui_gateway/methods_config_set.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tui_gateway/methods_config_set.py b/tui_gateway/methods_config_set.py index be03c00edb..7e5f2d4ffc 100644 --- a/tui_gateway/methods_config_set.py +++ b/tui_gateway/methods_config_set.py @@ -162,10 +162,6 @@ _FAST_WORDS = {"fast": "fast", "on": "fast", "normal": "normal", "off": "normal" def _set_fast(rid, params, key, value, session): - if session is None and params.get("session_id"): - # A runtime id this backend no longer holds (reaped / re-minted) must not reach the sessionless - # branch: it persists agent.service_tier for every surface. 4001 lets the client resume it. - return _sess_nowait(params, rid)[1] raw = _word(value) agent = session.get("agent") if session else None if agent is not None: @@ -282,10 +278,6 @@ def _set_yolo(rid, params, key, value, session): enable = _BOOL_WORDS.get(raw, not is_session_yolo_enabled(skey)) (enable_session_yolo if enable else disable_session_yolo)(skey) _emit_session_info(params.get("session_id", ""), session) - elif params.get("session_id"): - # Stale runtime id: the process flag below never reaches this session's approvals, yet every - # child spawned afterwards (compute host, a terminal's `hermes`) inherits the flip. - return _sess_nowait(params, rid)[1] else: enable = _BOOL_WORDS.get(raw, not is_truthy_value(os.environ.get("HERMES_YOLO_MODE"))) if enable: @@ -479,12 +471,21 @@ _CONFIG_SETTERS = { "cwd": _set_cwd, "terminal.cwd": _set_cwd, "workdir": _set_cwd, "prompt": _set_prompt, "personality": _set_personality, "skin": _set_skin} +# Keys whose sessionless branch writes a different, wider scope than the session branch (config.yaml's +# agent.* for every surface, the process env every later child inherits). A non-empty session_id this +# backend no longer holds (reaped / re-minted) is a stale session, not "no session": it answers 4001 so +# the client resumes, never the global write. An explicit scope="global" is still honoured. +_SESSION_SCOPED_KEYS = frozenset({"model", "fast", "yolo", "reasoning"}) + @method("config.set") @_profile_scoped def _(rid, params: dict) -> dict: key, value = params.get("key", ""), params.get("value", "") session = _sessions.get(params.get("session_id", "")) + if session is None and params.get("session_id") and key in _SESSION_SCOPED_KEYS \ + and _word(params.get("scope")) != "global": + return _sess_nowait(params, rid)[1] handler = _CONFIG_SETTERS.get(key) if handler is None and key.startswith("details_mode."): handler = _set_details_section