From 90970d85f2360bf0060db49f92bccba6830aeb0f Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:25:46 +0530 Subject: [PATCH] fix(gateway): a repeated /model --once keeps the earliest restore target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/model X --once` then `/model Y --once` before any turn replaced the pending snapshot with one taken while X was live, so slot cleanup restored X and made the first temporary model permanent (ehz0ah, review on #106966). setdefault keeps the snapshot from the first command — the user's standing override — as the restore target. One producer-driven regression test. --- gateway/slash_commands_model.py | 6 ++++-- tests/gateway/test_model_switch_persistence.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/gateway/slash_commands_model.py b/gateway/slash_commands_model.py index 82bb31ab41..875707a874 100644 --- a/gateway/slash_commands_model.py +++ b/gateway/slash_commands_model.py @@ -269,8 +269,10 @@ class GatewayModelCommandsMixin: if one_turn: if not hasattr(self, "_pending_one_turn_model_restores"): self._pending_one_turn_model_restores = {} - self._pending_one_turn_model_restores[ctx.session_key] = dict( - ctx.restore_snapshot or {"had_override": False, "override": None} + # A repeated --once before the turn runs must keep the EARLIEST snapshot: the later + # command's snapshot is the first temporary model, not the user's standing override. + self._pending_one_turn_model_restores.setdefault( + ctx.session_key, dict(ctx.restore_snapshot or {"had_override": False, "override": None}), ) elif not picker and hasattr(self, "_pending_one_turn_model_restores"): self._pending_one_turn_model_restores.pop(ctx.session_key, None) diff --git a/tests/gateway/test_model_switch_persistence.py b/tests/gateway/test_model_switch_persistence.py index a668518a3e..79f2b7b695 100644 --- a/tests/gateway/test_model_switch_persistence.py +++ b/tests/gateway/test_model_switch_persistence.py @@ -282,3 +282,19 @@ class TestOneTurnNeverPersisted: # ...but NEVER written through to the persistent session store. runner.async_session_store.set_model_override.assert_not_awaited() + @pytest.mark.asyncio + async def test_repeated_once_keeps_the_earliest_restore_target(self, tmp_path, monkeypatch): + """`/model X --once` then `/model Y --once` before any turn: the pending snapshot must still + be the user's standing override (none here), not X — otherwise slot cleanup would make the + first temporary model permanent.""" + runner = self._runner_with_store(tmp_path, monkeypatch) + sk = build_session_key(_make_source()) + + await runner._handle_model_command(self._event("/model gpt-5.5 --once")) + assert runner._session_model_overrides[sk]["model"] == "gpt-5.5" + await runner._handle_model_command(self._event("/model gpt-5.5 --once")) + + # The second producer call snapshotted the live gpt-5.5 override; the pending restore + # must still be the ORIGINAL "no override" state. + assert runner._pending_one_turn_model_restores[sk]["had_override"] is False +