diff --git a/agent/transports/codex_app_server_session.py b/agent/transports/codex_app_server_session.py index d097fed6ae9..7292823766e 100644 --- a/agent/transports/codex_app_server_session.py +++ b/agent/transports/codex_app_server_session.py @@ -604,6 +604,19 @@ class CodexAppServerSession: f"turn ended status={turn_status}", err_msg ) + if ( + not turn_complete + and not result.interrupted + and result.final_text + and result.error is None + ): + logger.warning( + "codex app-server turn reached deadline after a completed " + "assistant message but before turn/completed; accepting " + "the assistant text as the terminal response" + ) + turn_complete = True + if not turn_complete and not result.interrupted: # Hit the deadline. Issue interrupt to stop wasted compute, and # tell the caller to retire the session — a turn that never diff --git a/tests/agent/transports/test_codex_app_server_session.py b/tests/agent/transports/test_codex_app_server_session.py index cc40bb1862f..0322fb08721 100644 --- a/tests/agent/transports/test_codex_app_server_session.py +++ b/tests/agent/transports/test_codex_app_server_session.py @@ -728,6 +728,33 @@ class TestSessionRetirement: r = s.run_turn("hi", turn_timeout=1.0) assert r.should_retire is False + def test_final_agent_message_without_turn_completed_is_recovered(self): + """A completed assistant item is still a usable terminal response when + codex omits turn/completed and then goes quiet. + """ + client = FakeClient() + client.queue_notification( + "item/completed", + item={"type": "agentMessage", "id": "m1", "text": "done"}, + threadId="t", + turnId="tu1", + ) + s = make_session(client) + r = s.run_turn( + "hi", + turn_timeout=0.05, + notification_poll_timeout=0.01, + ) + assert r.final_text == "done" + assert r.interrupted is False + assert r.error is None + assert r.should_retire is False + assert any( + msg["role"] == "assistant" and msg.get("content") == "done" + for msg in r.projected_messages + ) + assert not any(method == "turn/interrupt" for method, _ in client.requests) + def test_post_tool_quiet_watchdog_trips_and_retires(self): client = FakeClient() # One tool completion, then total silence — no further events,