Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions agent/transports/codex_app_server_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,19 @@ def run_turn(
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
Expand Down
27 changes: 27 additions & 0 deletions tests/agent/transports/test_codex_app_server_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,33 @@ def test_completed_turn_does_not_retire(self):
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,
Expand Down