fix: session-scoped /fast + full /new reset to config defaults (#67979)

* fix(fast): default /fast to session scope on CLI and gateway

Completes the session-first policy from #67946 for the /fast toggle
(the remaining half of #54084). A bare /fast fast|normal now applies to
the current session only; --global persists agent.service_tier to
config.yaml.

Gateway: new _session_service_tier_overrides dict (registered in
_CONVERSATION_SCOPED_STATE so /new clears it) resolved at both agent
turn sites via _resolve_session_service_tier(); the /fast handler and
its choice picker apply session overrides and evict the cached agent.
The TUI config.set fast path was already session-scoped.

CLI: /fast parses --global (parity with /reasoning); bare toggles
mutate self.service_tier only.

* fix(sessions): /new resets model, reasoning, and fast to config defaults

/new and /reset are full conversation boundaries: session-scoped
runtime overrides do not carry into the next session (#48055, #23131).

CLI new_session(): clears the one-turn model restore, re-derives
service_tier from config, and — when the session's model differs from
the config default — switches back via the shared switch_model()
pipeline (live agent swap included; best-effort so an unreachable
default never blocks /new).

TUI _reset_session_agent(): stops forwarding model_override /
create_reasoning_override / create_service_tier_override into the
rebuilt agent and pops the pins so later rebuilds can't resurrect
them. The gateway already cleared its per-session overrides via
_clear_conversation_scope on /new.

Cross-session contamination stays impossible: nothing here touches
process-global env or other sessions' pins.
This commit is contained in:
Teknium 2026-07-20 03:27:22 -07:00 committed by GitHub
parent 3441b80f4f
commit 47fb20c0bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 332 additions and 42 deletions

62
cli.py
View File

@ -7154,6 +7154,68 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self.reasoning_config = _parse_reasoning_config(
CLI_CONFIG["agent"].get("reasoning_effort", "")
)
# /new is a full conversation boundary: session-scoped runtime
# overrides (/model --session, /fast, one-turn restores) do not carry
# forward. Re-derive model/provider and service tier from config.yaml
# so a session-only switch never leaks into the next session (#48055,
# #23131).
self._pending_one_turn_model_restore = None
self.service_tier = _parse_service_tier_config(
CLI_CONFIG["agent"].get("service_tier", "")
)
_model_config = CLI_CONFIG.get("model", {})
_config_model = (
(_model_config.get("default") or _model_config.get("model") or "")
if isinstance(_model_config, dict)
else (_model_config or "")
)
if _config_model and _config_model != getattr(self, "model", None):
_config_provider = (
_model_config.get("provider", "")
if isinstance(_model_config, dict)
else ""
)
try:
from hermes_cli.model_switch import switch_model as _switch_model
_reset_result = _switch_model(
raw_input=_config_model,
current_provider=self.provider or "",
current_model=self.model or "",
current_base_url=self.base_url or "",
current_api_key=self.api_key or "",
is_global=False,
explicit_provider=_config_provider or "",
)
if _reset_result.success:
if self.agent:
self.agent.switch_model(
new_model=_reset_result.new_model,
new_provider=_reset_result.target_provider,
api_key=_reset_result.api_key,
base_url=_reset_result.base_url,
api_mode=_reset_result.api_mode,
)
self.model = _reset_result.new_model
self.provider = _reset_result.target_provider
self.requested_provider = _reset_result.target_provider
self._explicit_api_key = _reset_result.api_key
self._explicit_base_url = _reset_result.base_url
if _reset_result.api_key:
self.api_key = _reset_result.api_key
if _reset_result.base_url:
self.base_url = _reset_result.base_url
if _reset_result.api_mode:
self.api_mode = _reset_result.api_mode
if not silent:
_cprint(
f" (model reset to config default: "
f"{_reset_result.new_model})"
)
except Exception:
# Best-effort: an unreachable config default must never block
# /new. The session keeps the current working model.
logger.debug("/new model reset to config default failed", exc_info=True)
_sync_process_session_id(self.session_id)
if self.agent:

View File

@ -2031,6 +2031,7 @@ _CONVERSATION_SCOPED_STATE: tuple = (
"_session_model_overrides",
"_pending_one_turn_model_restores",
"_session_reasoning_overrides",
"_session_service_tier_overrides",
"_pending_model_notes",
"_last_resolved_model",
"_queued_events",
@ -3055,6 +3056,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
_session_model_overrides: Dict[str, Dict[str, str]] = {}
_pending_one_turn_model_restores: Dict[str, Dict[str, Any]] = {}
_session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}
_session_service_tier_overrides: Dict[str, Optional[str]] = {}
_pending_turn_sidecar_notes: Dict[str, List[str]] = {}
_session_ephemeral_pin: Dict[str, tuple] = {}
_session_vc_last: Dict[str, str] = {}
@ -3264,6 +3266,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
# Per-session reasoning effort overrides from /reasoning.
# Key: session_key, Value: parsed reasoning config dict.
self._session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}
# Per-session fast-mode overrides from /fast.
# Key: session_key, Value: "priority" or None (explicit normal).
self._session_service_tier_overrides: Dict[str, Optional[str]] = {}
# Per-turn must-deliver notes relocated out of the ephemeral system
# prompt (auto-reset note, first-contact intro, voice-channel change).
# Staged by _handle_message_with_agent, consumed once by run_sync and
@ -5238,6 +5243,52 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
else:
self._session_reasoning_overrides[session_key] = dict(reasoning_config)
def _resolve_session_service_tier(
self,
source=None,
session_key: Optional[str] = None,
) -> Optional[str]:
"""Resolve the effective service tier for a session.
A session-scoped /fast override wins over the config default. The
override dict stores "priority" or None (explicit normal), so key
presence not value truthiness decides whether it applies.
"""
resolved_session_key = session_key
if not resolved_session_key and source is not None:
try:
resolved_session_key = self._session_key_for_source(source)
except Exception:
resolved_session_key = None
overrides = getattr(self, "_session_service_tier_overrides", {}) or {}
if resolved_session_key and resolved_session_key in overrides:
return overrides[resolved_session_key]
return self._load_service_tier()
def _set_session_service_tier_override(
self,
session_key: str,
service_tier,
clear: bool = False,
) -> None:
"""Set or clear the session-scoped /fast override.
``service_tier`` is "priority" or None (explicit normal). Pass
``clear=True`` to remove the override entirely (fall back to config).
"""
if not session_key:
return
if "_session_service_tier_overrides" not in self.__dict__:
# Force an instance-level dict: the class attribute is a shared
# default for partially-constructed test runners, and mutating it
# would leak overrides across runner instances.
self._session_service_tier_overrides = {}
if clear:
self._session_service_tier_overrides.pop(session_key, None)
else:
self._session_service_tier_overrides[session_key] = service_tier
@staticmethod
def _load_service_tier() -> str | None:
"""Load Priority Processing setting from config.yaml.
@ -14702,7 +14753,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
source=source, model=model
)
self._reasoning_config = reasoning_config
self._service_tier = self._load_service_tier()
self._service_tier = self._resolve_session_service_tier(source=source)
turn_route = self._resolve_turn_agent_config(prompt, model, runtime_kwargs)
# Enrich the prompt with image descriptions so the background
@ -19917,7 +19968,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
model=model,
)
self._reasoning_config = reasoning_config
self._service_tier = self._load_service_tier()
self._service_tier = self._resolve_session_service_tier(
source=source, session_key=session_key
)
# Set up stream consumer for token streaming or interim commentary.
_stream_consumer = None
_stream_delta_cb = None

View File

@ -3108,43 +3108,64 @@ class GatewaySlashCommandsMixin:
return out
async def _handle_fast_command(self, event: MessageEvent) -> Optional[str]:
"""Handle /fast — mirror the CLI Priority Processing toggle in gateway chats."""
"""Handle /fast — mirror the CLI Priority Processing toggle in gateway chats.
Session-scoped by default; ``--global`` persists agent.service_tier
to config.yaml (parity with /model and /reasoning).
"""
from gateway.run import _load_gateway_config, _resolve_gateway_model
from hermes_cli.models import model_supports_fast_mode
args = event.get_command_args().strip().lower()
self._service_tier = self._load_service_tier()
raw_args = event.get_command_args().strip().lower()
# Reuse the /reasoning arg parser: strips --global (any position),
# normalizes unicode dashes.
args, persist_global = self._parse_reasoning_command_args(raw_args)
session_key = self._session_key_for_source(event.source)
self._service_tier = self._resolve_session_service_tier(
session_key=session_key
)
user_config = _load_gateway_config()
model = _resolve_gateway_model(user_config)
if not model_supports_fast_mode(model):
return t("gateway.fast.not_supported")
def _apply_fast_selection(value: str) -> str:
def _apply_fast_selection(value: str, persist: bool = False) -> str:
"""Apply a /fast argument (typed or picked) and return the reply."""
if value in {"fast", "on"}:
self._service_tier = "priority"
tier = "priority"
saved_value = "fast"
label = t("gateway.fast.label_fast")
elif value in {"normal", "off"}:
self._service_tier = None
tier = None
saved_value = "normal"
label = t("gateway.fast.label_normal")
else:
return t("gateway.fast.unknown_arg", arg=value)
if self._save_gateway_config_key("agent.service_tier", saved_value):
return t("gateway.fast.saved", label=label)
self._service_tier = tier
if persist:
if self._save_gateway_config_key("agent.service_tier", saved_value):
# Global write supersedes any session override.
self._set_session_service_tier_override(
session_key, None, clear=True
)
self._evict_cached_agent(session_key)
return t("gateway.fast.saved", label=label)
# Config write failed — fall back to a session override so the
# user's choice still applies (mirrors /reasoning --global).
self._set_session_service_tier_override(session_key, tier)
self._evict_cached_agent(session_key)
return t("gateway.fast.session_only", label=label)
self._set_session_service_tier_override(session_key, tier)
self._evict_cached_agent(session_key)
return t("gateway.fast.session_only", label=label)
if not args or args == "status":
is_fast = self._service_tier == "priority"
status = t("gateway.fast.status_fast") if is_fast else t("gateway.fast.status_normal")
# Interactive picker on platforms that support it.
session_key = self._session_key_for_source(event.source)
async def _on_fast_choice(_chat_id: str, value: str) -> str:
return _apply_fast_selection(value)
return _apply_fast_selection(value, persist=persist_global)
picker_sent = await self._try_send_choice_picker(
event,
@ -3169,7 +3190,7 @@ class GatewaySlashCommandsMixin:
return t("gateway.fast.status", mode=status)
return _apply_fast_selection(args)
return _apply_fast_selection(args, persist=persist_global)
async def _handle_yolo_command(self, event: MessageEvent) -> Union[str, EphemeralReply]:
"""Handle /yolo — toggle dangerous command approval bypass for this session only."""

View File

@ -2653,7 +2653,11 @@ class CLICommandsMixin:
_cprint(f" {_ACCENT}✓ Busy input mode set to '{arg}' (session only){_RST}")
def _handle_fast_command(self, cmd: str):
"""Handle /fast — toggle fast mode (OpenAI Priority Processing / Anthropic Fast Mode)."""
"""Handle /fast — toggle fast mode (OpenAI Priority Processing / Anthropic Fast Mode).
Session-scoped by default; ``--global`` persists agent.service_tier
to config.yaml (parity with /model and /reasoning).
"""
from cli import _ACCENT, _DIM, _RST, _cprint, save_config_value
if not self._fast_command_available():
_cprint(" (._.) /fast is only available for models that support fast mode (OpenAI Priority Processing or Anthropic Fast Mode).")
@ -2672,10 +2676,15 @@ class CLICommandsMixin:
if len(parts) < 2 or parts[1].strip().lower() == "status":
status = "fast" if self.service_tier == "priority" else "normal"
_cprint(f" {_ACCENT}{feature_name}: {status}{_RST}")
_cprint(f" {_DIM}Usage: /fast [normal|fast|status]{_RST}")
_cprint(f" {_DIM}Usage: /fast [normal|fast|status] [--global]{_RST}")
return
arg = parts[1].strip().lower()
arg_tokens = parts[1].strip().lower().split()
explicit_global = "--global" in arg_tokens
arg = " ".join(
token for token in arg_tokens
if token not in ("--global", "--session")
)
if arg in {"fast", "on"}:
self.service_tier = "priority"
@ -2687,14 +2696,16 @@ class CLICommandsMixin:
label = "NORMAL"
else:
_cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}")
_cprint(f" {_DIM}Usage: /fast [normal|fast|status]{_RST}")
_cprint(f" {_DIM}Usage: /fast [normal|fast|status] [--global]{_RST}")
return
self.agent = None # Force agent re-init with new service-tier config
if save_config_value("agent.service_tier", saved_value):
if explicit_global and save_config_value("agent.service_tier", saved_value):
_cprint(f" {_ACCENT}{feature_name} set to {label} (saved to config){_RST}")
elif explicit_global:
_cprint(f" {_ACCENT}{feature_name} set to {label} (session only; config save failed){_RST}")
else:
_cprint(f" {_ACCENT}{feature_name} set to {label} (session only){_RST}")
_cprint(f" {_ACCENT}{feature_name} set to {label} (this session — use --global to persist){_RST}")
def _handle_debug_command(self, cmd_original: str = ""):
"""Handle /debug — upload debug report + logs and print share URLs.

View File

@ -156,8 +156,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
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")),
args_hint="[normal|fast|status] [--global]",
subcommands=("normal", "fast", "status", "on", "off", "--global")),
CommandDef("skin", "Show or change the display skin/theme", "Configuration",
cli_only=True, args_hint="[name]"),
CommandDef("indicator", "Pick the TUI busy-indicator style", "Configuration",

View File

@ -83,6 +83,20 @@ class TestHandleFastCommand(unittest.TestCase):
):
cli_mod.HermesCLI._handle_fast_command(stub, "/fast normal")
# Session-scoped by default: no config write.
mock_save.assert_not_called()
self.assertIsNone(stub.service_tier)
self.assertIsNone(stub.agent)
def test_global_flag_persists_service_tier(self):
cli_mod = _import_cli()
stub = self._make_cli(service_tier="priority")
with (
patch.object(cli_mod, "_cprint"),
patch.object(cli_mod, "save_config_value", return_value=True) as mock_save,
):
cli_mod.HermesCLI._handle_fast_command(stub, "/fast normal --global")
mock_save.assert_called_once_with("agent.service_tier", "normal")
self.assertIsNone(stub.service_tier)
self.assertIsNone(stub.agent)

View File

@ -220,6 +220,63 @@ class TestHandleReasoningCommand(unittest.TestCase):
self.assertEqual(agent.reasoning_config, {"enabled": True, "effort": "medium"})
agent.reset_session_state.assert_called_once()
def test_new_session_resets_service_tier_and_model_from_config(self):
"""/new re-derives service tier and model from config.yaml — session
/fast and /model switches do not carry forward (#48055, #23131)."""
from cli import CLI_CONFIG, HermesCLI
agent = SimpleNamespace(
reasoning_config=None,
reset_session_state=MagicMock(),
switch_model=MagicMock(),
)
stub = SimpleNamespace(
agent=agent,
conversation_history=[],
session_id="old-session",
_session_db=None,
_pending_title=None,
_resumed=False,
reasoning_config=None,
_notify_session_boundary=MagicMock(),
# Session had switched to fast + a session-only model.
service_tier="priority",
_pending_one_turn_model_restore={"model": "stale"},
model="session-switched-model",
provider="openrouter",
requested_provider="openrouter",
api_key="k",
base_url="",
api_mode="",
)
fake_result = SimpleNamespace(
success=True,
new_model="config-default-model",
target_provider="openrouter",
api_key="k2",
base_url="https://openrouter.ai/api/v1",
api_mode="chat_completions",
)
with patch.dict(
CLI_CONFIG.setdefault("agent", {}),
{"reasoning_effort": "medium", "service_tier": "normal"},
), patch.dict(
CLI_CONFIG,
{"model": {"default": "config-default-model", "provider": "openrouter"}},
), patch(
"hermes_cli.model_switch.switch_model", return_value=fake_result
):
HermesCLI.new_session(stub, silent=True)
# Fast override cleared back to config default (normal → None).
self.assertIsNone(stub.service_tier)
# One-turn restore snapshot cleared.
self.assertIsNone(stub._pending_one_turn_model_restore)
# Model reset to the config default via the live agent swap.
self.assertEqual(stub.model, "config-default-model")
agent.switch_model.assert_called_once()
# ---------------------------------------------------------------------------
# Reasoning extraction and result dict

View File

@ -189,7 +189,8 @@ class TestFastChoicePicker:
assert values == ["fast", "normal"]
@pytest.mark.asyncio
async def test_fast_picker_selection_persists_service_tier(self, tmp_path, monkeypatch):
async def test_fast_picker_selection_is_session_scoped(self, tmp_path, monkeypatch):
"""A bare /fast picker tap applies a session override, not a config write."""
self._patch_fast_support(monkeypatch, tmp_path)
adapter = _PickerAdapter()
runner = _make_runner(adapter)
@ -199,6 +200,22 @@ class TestFastChoicePicker:
on_choice = adapter.calls[0]["on_choice_selected"]
await on_choice(event.source.chat_id, "fast")
assert runner._service_tier == "priority"
assert runner._session_service_tier_overrides
assert not (tmp_path / "config.yaml").exists()
@pytest.mark.asyncio
async def test_fast_picker_global_flag_persists_service_tier(self, tmp_path, monkeypatch):
"""A /fast --global picker tap persists agent.service_tier to config."""
self._patch_fast_support(monkeypatch, tmp_path)
adapter = _PickerAdapter()
runner = _make_runner(adapter)
event = _make_event("/fast --global")
await runner._handle_fast_command(event)
on_choice = adapter.calls[0]["on_choice_selected"]
await on_choice(event.source.chat_id, "fast")
assert runner._service_tier == "priority"
saved = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
assert saved["agent"]["service_tier"] == "fast"

View File

@ -143,7 +143,8 @@ def test_turn_route_skips_priority_processing_for_unsupported_models():
@pytest.mark.asyncio
async def test_handle_fast_command_persists_config(monkeypatch, tmp_path):
async def test_handle_fast_command_session_scoped_by_default(monkeypatch, tmp_path):
"""Bare /fast fast applies a session override — config.yaml untouched."""
runner = _make_runner()
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
@ -152,11 +153,57 @@ async def test_handle_fast_command_persists_config(monkeypatch, tmp_path):
response = await runner._handle_fast_command(_make_event("/fast fast"))
assert "FAST" in response
assert runner._service_tier == "priority"
# Session override recorded; config.yaml NOT written.
assert runner._session_service_tier_overrides
assert not (tmp_path / "config.yaml").exists()
@pytest.mark.asyncio
async def test_handle_fast_command_global_flag_persists_config(monkeypatch, tmp_path):
runner = _make_runner()
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
response = await runner._handle_fast_command(_make_event("/fast fast --global"))
assert "FAST" in response
assert runner._service_tier == "priority"
saved = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
assert saved["agent"]["service_tier"] == "fast"
# Global write supersedes the session override.
assert not runner._session_service_tier_overrides
@pytest.mark.asyncio
async def test_session_fast_override_beats_config_default(monkeypatch, tmp_path):
"""A session /fast normal wins over agent.service_tier: fast in config."""
runner = _make_runner()
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
monkeypatch.setattr(
gateway_run,
"_load_gateway_runtime_config",
lambda: {"agent": {"service_tier": "fast"}},
)
monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
event = _make_event("/fast normal")
session_key = runner._session_key_for_source(event.source)
response = await runner._handle_fast_command(event)
assert "NORMAL" in response
# Override stores explicit None (normal) and wins over config "fast".
assert session_key in runner._session_service_tier_overrides
assert runner._resolve_session_service_tier(session_key=session_key) is None
# A different session still gets the config default.
assert runner._resolve_session_service_tier(session_key="other-session") == "priority"
@pytest.mark.asyncio

View File

@ -10459,16 +10459,20 @@ def test_start_agent_build_passes_session_model_override(
# ── billing/subscription state + error serialization ─────────────────
def test_reset_session_agent_preserves_explicit_normal_fast(monkeypatch):
def test_reset_session_agent_clears_session_overrides(monkeypatch):
"""/new is a full conversation boundary: session-scoped /model, /reasoning,
and /fast overrides do NOT carry into the fresh agent it re-derives
everything from config.yaml (#48055, #23131)."""
captured = {}
new_agent = types.SimpleNamespace(model="openai/gpt-5.4", service_tier="")
session = _session(
agent=types.SimpleNamespace(
model="openai/gpt-5.4",
reasoning_config=None,
reasoning_config={"enabled": True, "effort": "high"},
service_tier="",
),
model_override={"model": "openai/gpt-5.4"},
create_reasoning_override={"enabled": True, "effort": "high"},
create_service_tier_override="",
)
@ -10488,7 +10492,14 @@ def test_reset_session_agent_preserves_explicit_normal_fast(monkeypatch):
server._reset_session_agent("sid", session)
assert captured["service_tier_override"] == ""
# No session overrides forwarded — fresh agent builds from config.
assert "model_override" not in captured
assert "reasoning_config_override" not in captured
assert "service_tier_override" not in captured
# And the session pins are gone so a later rebuild can't resurrect them.
assert "model_override" not in session
assert "create_reasoning_override" not in session
assert "create_service_tier_override" not in session
assert session["agent"] is new_agent

View File

@ -4765,25 +4765,22 @@ def _preview_restart_callbacks(parent: str, task_id: str) -> dict:
def _reset_session_agent(sid: str, session: dict) -> dict:
tokens = _set_session_context(session["session_key"])
try:
# Preserve this session's chosen model AND reasoning across /new so a
# reset doesn't silently revert to global config (or to a model
# another session set). See the cross-session-contamination note in
# _apply_model_switch.
reset_kw = {"model_override": session.get("model_override")}
old_reasoning = getattr(session.get("agent"), "reasoning_config", None)
if old_reasoning is None:
old_reasoning = session.get("create_reasoning_override")
if isinstance(old_reasoning, dict):
reset_kw["reasoning_config_override"] = old_reasoning
create_service_tier_override = session.get("create_service_tier_override")
if create_service_tier_override is not None:
reset_kw["service_tier_override"] = create_service_tier_override
# /new is a full conversation boundary: session-scoped runtime
# overrides (/model, /reasoning, /fast) do NOT carry forward — the
# fresh agent re-derives model/provider, reasoning, and service tier
# from config.yaml (#48055, #23131). Session pins are cleared below so
# a rebuild can't resurrect them. (Global process state is still never
# touched — see the cross-session-contamination note in
# _apply_model_switch.)
session.pop("model_override", None)
session.pop("create_reasoning_override", None)
session.pop("create_service_tier_override", None)
session.pop("one_turn_model_restore", None)
new_agent = _make_agent(
sid,
session["session_key"],
session_id=session["session_key"],
platform_override=_session_source(session),
**reset_kw,
)
finally:
_clear_session_context(tokens)