Skip to content
Merged
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
22 changes: 12 additions & 10 deletions src/hmz/agents/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
):
Expand Down
15 changes: 15 additions & 0 deletions tests/agents/test_appservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down