diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index 8c7cd95c4f..c83eb060ee 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -679,7 +679,7 @@ def run_codex_app_server_turn(agent, *, user_message: str, original_user_message _ensure_codex_session(agent, messages) try: _start_codex_thread(agent) - turn = agent._codex_session.run_turn(user_input=user_message) + turn = agent._codex_session.run_turn(user_input=user_message, model=_codex_wire_model(agent)) except Exception as exc: logger.exception("codex app-server turn failed") _close_codex_session(agent) diff --git a/agent/transports/codex_app_server_session.py b/agent/transports/codex_app_server_session.py index 86287403d9..04b056f550 100644 --- a/agent/transports/codex_app_server_session.py +++ b/agent/transports/codex_app_server_session.py @@ -444,11 +444,14 @@ class CodexAppServerSession: return projection, aborted def run_turn( - self, user_input: Any, *, turn_timeout: float = 600.0, + self, user_input: Any, *, model: Optional[str] = None, turn_timeout: float = 600.0, notification_poll_timeout: float = 0.25, post_tool_quiet_timeout: float = 90.0, ) -> TurnResult: """Send a user message and block until turn/completed, bridging approvals and projecting items. + model: sent on ``turn/start`` (codex applies it to this and later turns), so an in-place ``/model`` + switch reaches a thread that was started with another model. + post_tool_quiet_timeout: if codex emits a tool completion and then goes quiet for this many seconds without emitting another item or `turn/completed`, log a warning (once per tool result) and keep waiting. Wire silence is not evidence of a wedged process: after a large tool output codex can @@ -463,11 +466,10 @@ class CodexAppServerSession: result.interrupted = True else: input_items, result.submitted_user_text = _build_turn_input(user_input) - ts = self._request_for( - result, "turn/start", - {"threadId": self._thread_id, "input": input_items}, - "turn/start", - ) + params: dict[str, Any] = {"threadId": self._thread_id, "input": input_items} + if model: + params["model"] = model + ts = self._request_for(result, "turn/start", params, "turn/start") if ts is not None: self._run_started_turn(result, ts, turn_timeout, notification_poll_timeout, post_tool_quiet_timeout) self._interrupt_event.clear() diff --git a/tests/agent/test_codex_runtime_prompt_handoff.py b/tests/agent/test_codex_runtime_prompt_handoff.py index 2e67658b65..49120c5cea 100644 --- a/tests/agent/test_codex_runtime_prompt_handoff.py +++ b/tests/agent/test_codex_runtime_prompt_handoff.py @@ -73,3 +73,31 @@ def test_runtime_retires_thread_when_prompt_composition_changes(monkeypatch): starts = [p["developerInstructions"] for (m, p) in client.requests if m == "thread/start"] assert starts == ["SOUL: you are Hermes\n\nAlways start with ZZZ", "SOUL: you are Hermes\n\nPersonality: pirate"] assert client.closed == 1 # the stale thread's client was closed, not leaked + + +def test_each_turn_sends_the_agent_s_current_wire_model(monkeypatch): + """CLI/TUI ``/model`` mutates the live agent; every turn/start carries the model selected now (the + ``-900k`` alias as its base slug), not the one the thread was started with.""" + turns = [] + + class _Session: + def __init__(self, **_kw): + pass + + def ensure_started(self): + return "t1" + + def run_turn(self, **kw): + turns.append(kw["model"]) + raise RuntimeError("stop after turn/start") + + def close(self): + pass + + monkeypatch.setattr(sess_mod, "CodexAppServerSession", _Session) + agent = _agent(model="gpt-5.6-sol-900k", _interrupt_requested=False, _interrupt_message=None) + for model in ("gpt-5.6-sol-900k", "gpt-5.5"): + agent.model = model + codex_runtime.run_codex_app_server_turn(agent, user_message="hi", original_user_message="hi", + messages=[], effective_task_id="t") + assert turns == ["gpt-5.6-sol", "gpt-5.5"] diff --git a/tests/agent/transports/test_codex_app_server_session.py b/tests/agent/transports/test_codex_app_server_session.py index c47a456f82..b003aff750 100644 --- a/tests/agent/transports/test_codex_app_server_session.py +++ b/tests/agent/transports/test_codex_app_server_session.py @@ -311,6 +311,18 @@ class TestRunTurn: assert result.submitted_user_text == params["input"][0]["text"] assert result.submitted_user_text != rich_input + def test_turn_start_carries_the_turn_model_only_when_given(self): + """An in-place ``/model`` switch keeps the session; the model rides on turn/start, which codex + applies to this and later turns of the existing thread.""" + sent = [] + for model in ("gpt-5.5", None): + client = FakeClient() + client.queue_notification("turn/completed", threadId="t", + turn={"id": "turn-fake-001", "status": "completed", "error": None}) + make_session(client).run_turn("hi", model=model, turn_timeout=2.0) + sent.append(next(p for (m, p) in client.requests if m == "turn/start").get("model")) + assert sent == ["gpt-5.5", None] + def test_foreign_completion_in_server_request_drain_is_ignored(self): """Approval draining must not project a child result into the parent.""" client = FakeClient()