diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 9f30b698fc9..aa523158d17 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -3233,6 +3233,20 @@ class GatewaySlashCommandsMixin: return t("gateway.resume.switch_failed") self._clear_session_boundary_security_state(session_key) + # Clear session-scoped model/reasoning overrides so the resumed + # conversation picks up configured defaults instead of a /model + # switch made in the previous session under the same chat + # session_key. /resume is a conversation boundary just like /new + # (which clears these too); without this, a stale override leaks + # across the switch. See #10702. + _overrides = getattr(self, "_session_model_overrides", None) + if isinstance(_overrides, dict): + _overrides.pop(session_key, None) + self._set_session_reasoning_override(session_key, None) + _pending_notes = getattr(self, "_pending_model_notes", None) + if isinstance(_pending_notes, dict): + _pending_notes.pop(session_key, None) + # Evict any cached agent for this session so the next message # rebuilds with the correct session_id end-to-end — mirrors # /branch and /reset. Without this, the cached AIAgent (and its diff --git a/tests/gateway/test_resume_command.py b/tests/gateway/test_resume_command.py index a24a8578f49..02ac2a449c8 100644 --- a/tests/gateway/test_resume_command.py +++ b/tests/gateway/test_resume_command.py @@ -173,6 +173,40 @@ class TestHandleResumeCommand: assert call_args[0][1] == "old_session_abc" db.close() + @pytest.mark.asyncio + async def test_resume_clears_session_model_overrides(self, tmp_path): + """Resume must not carry a previous session's /model override into the + restored conversation, while leaving other chats' overrides intact (#10702).""" + from hermes_state import SessionDB + db = SessionDB(db_path=tmp_path / "state.db") + db.create_session("old_session_abc", "telegram") + db.set_session_title("old_session_abc", "My Project") + db.create_session("current_session_001", "telegram") + + event = _make_event(text="/resume My Project") + runner = _make_runner(session_db=db, current_session_id="current_session_001", + event=event) + key = _session_key_for_event(event) + runner._session_model_overrides = { + key: {"model": "gpt-5", "provider": "openai"}, + "agent:main:telegram:dm:other": {"model": "keep-me"}, + } + runner._pending_model_notes = { + key: "[Note: switched to gpt-5]", + "agent:main:telegram:dm:other": "[Note: keep-me]", + } + + result = await runner._handle_resume_command(event) + + assert "Resumed" in result + # The resumed chat's override + pending note are cleared... + assert key not in runner._session_model_overrides + assert key not in runner._pending_model_notes + # ...but an unrelated chat's state is untouched. + assert runner._session_model_overrides["agent:main:telegram:dm:other"] == {"model": "keep-me"} + assert runner._pending_model_notes["agent:main:telegram:dm:other"] == "[Note: keep-me]" + db.close() + @pytest.mark.asyncio async def test_resume_nonexistent_name(self, tmp_path): """Returns error for unknown session name."""