fix(gateway): clear session-scoped model overrides on /resume

/resume is a conversation boundary, but unlike /new it did not clear the
chat-keyed _session_model_overrides / _pending_model_notes. A /model switch
made in the previous session under the same chat session_key leaked into the
resumed conversation, running it on the wrong model.

Clear both maps for the session_key after the switch (mirroring /new), scoped
to that key so other chats' overrides are untouched. The cached-agent eviction
this leak also implied already landed via #6672.

Closes #10702.
This commit is contained in:
Junass1 2026-06-28 21:16:13 -07:00 committed by Teknium
parent 476875acb9
commit 61a4526ac7
2 changed files with 48 additions and 0 deletions

View file

@ -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

View file

@ -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."""