From 3ac713d81d776b21e802ea21e760d4fa449010f6 Mon Sep 17 00:00:00 2001 From: antoinegg1 <78747324+antoinegg1@users.noreply.github.com> Date: Thu, 27 Aug 2026 07:40:32 +0000 Subject: [PATCH] fix(agents): accept Codex turns after reconnect --- src/hmz/agents/codex.py | 22 ++++++++++++---------- tests/agents/test_appservers.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/hmz/agents/codex.py b/src/hmz/agents/codex.py index d28aca6..fdbf1d3 100644 --- a/src/hmz/agents/codex.py +++ b/src/hmz/agents/codex.py @@ -581,16 +581,18 @@ def turn(self, params: dict[str, Any], running: _Running) -> Iterator[Event]: running.spends(risen) case "error": failed = json.dumps(told.get("error")) - case "turn/completed" if told.get("turn", {}).get( - "status" - ) not in (None, "completed"): - # A failed or interrupted turn is complete even when the server - # does not follow it with a separate idle notification. - turn_said = told["turn"] - failed = json.dumps( - turn_said.get("error") or turn_said.get("status") - ) - break + case "turn/completed": + turn_said = cast("dict[str, Any]", told.get("turn") or {}) + if turn_said.get("status") not in (None, "completed"): + # A failed or interrupted turn is complete even when the + # server does not follow it with a separate idle notification. + failed = json.dumps( + turn_said.get("error") or turn_said.get("status") + ) + break + # Codex reports a reconnect attempt as an error notification even + # when a later sampling request completes this same turn. + failed = None case "thread/status/changed" if ( begun and told["status"]["type"] == "idle" ): diff --git a/tests/agents/test_appservers.py b/tests/agents/test_appservers.py index cabea82..9c8a1a2 100644 --- a/tests/agents/test_appservers.py +++ b/tests/agents/test_appservers.py @@ -201,6 +201,12 @@ def send(message): send({"method": "thread/status/changed", "params": {"status": {"type": "idle"}}}) if call["method"] == "turn/start": send({"method": "turn/started", "params": {"turnId": "turn_fake"}}) + if call["params"]["input"][0]["text"] == "recovering": + send({"method": "error", "params": {"error": { + "message": "Reconnecting... 1/5", + "codexErrorInfo": {"responseStreamDisconnected": { + "httpStatusCode": None}}, + }}}) if call["params"]["input"][0]["text"] == "asking": # A turn stopping to ask its user something, which the server puts to the client # as a request of its own and waits on. @@ -704,6 +710,15 @@ def test_a_codex_turn_says_what_it_is_doing_while_it_is_doing_it( assert ("tool", "WebSearch and the one after it") in shown +def test_a_codex_turn_that_reconnects_can_still_complete( + working: _FakeServer, +) -> None: + """A transient error is superseded when Codex completes the same turn.""" + session = CodexAgent(CodexAgentConfig(model="gpt-5-codex", effort="high")).new() + + assert session("recovering") == "done" + + def test_what_a_codex_turn_spent_is_charged_to_the_turn_that_spent_it( working: _FakeServer, ) -> None: