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 +