Codex Responses finish_reason misclassified as incomplete for reasoning-only turns
Cluster 64434. Data as of
Nothing open on GitHub. Fix merged in #64764 on 15 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #64764. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #64764. That is separate from the fix event. - Still open
- Nothing open on GitHub.
The members concern finish_reason being labeled "incomplete" for reasoning-only turns under api_mode=codex_responses, with one issue reporting multi-minute stalls and iteration budget exhaustion. Two items are fixes (one referencing #64434) that appear to propose trusting response.status on unrecognized Responses backends; their outcomes are not established here.
Open issues0
Open PRs0
Fixes on record
-
#64764 fix(codex): trust response.status for reasoning-only turns on unrecognized Responses backends
files:
agent/codex_responses_adapter.py,tests/agent/test_codex_responses_adapter.py,tests/run_agent/test_run_agent_codex_responses.pyShow the diff
-
agent/codex_responses_adapter.py@@ -1380,12 +1380,28 @@ def _normalize_codex_response( finish_reason = "incomplete" elif (reasoning_items_raw or reasoning_parts or saw_reasoning_item) and not final_text: # Response contains only reasoning (encrypted thinking state and/or - # human-readable summary) with no visible content or tool calls. The - # model is still thinking and needs another turn to produce the actual - # answer. Marking this as "stop" would send it into the empty-content - # retry loop which burns retries then fails — treat it as incomplete so - # the Codex continuation path handles it correctly. - finish_reason = "incomplete" + # human-readable summary) with no visible content or tool calls. + # + # For the specially-handled backends (Codex, xAI, GitHub/Copilot), + # reasoning-only with status="completed" means "the model is still + # thinking and needs another turn" — treat it as incomplete so the + # Codex continuation path retries instead of falling into the + # empty-content retry loop. + # + # For all other backends (other:<base_url>, etc.), trust the provider's + # own response.status signal. When status == "completed" and no items + # are queued/in_progress/incomplete, reasoning alone is a valid final + # state — forcing "incomplete" causes multi-minute stalls as the + # continuation path re-issues calls (3 retries × up to 240s each). + # See https://github.com/NousResearch/hermes-agent/issues/64434 + if response_status == "completed" and issuer_kind not in ( + "codex_backend", + "xai_responses", + "github_responses", + ): + finish_reason = "stop" + else: + finish_reason = "incomplete" else: finish_reason = "stop" return assistant_message, finish_reason -
tests/agent/test_codex_responses_adapter.py@@ -51,6 +51,12 @@ def test_normalize_codex_response_drops_transient_rs_tmp_reasoning_items(): def test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete(): + """Summary-only reasoning keeps the continuation path for Codex backends. + + Since #64434, an unrecognized issuer with ``response.status="completed"`` + trusts the provider and returns ``stop`` — so this test pins the Codex + backend explicitly, where reasoning-only still means "still thinking". + """ response = SimpleNamespace( status="completed", output=[ @@ -63,7 +69,9 @@ def test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete(): ], ) - assistant_message, finish_reason = _normalize_codex_response(response) + assistant_message, finish_reason = _normalize_codex_response( + response, issuer_kind="codex_backend" + ) assert finish_reason == "incomplete" assert assistant_message.content == "" -
tests/run_agent/test_run_agent_codex_responses.py@@ -2359,15 +2359,16 @@ def _codex_reasoning_only_response(*, encrypted_content="enc_abc123", summary_te def test_normalize_codex_response_marks_reasoning_only_as_incomplete(monkeypatch): - """A response with only reasoning items and no content should be 'incomplete', not 'stop'. + """A response with only reasoning items and no content should be 'incomplete' for Codex backends. - Without this fix, reasoning-only responses get finish_reason='stop' which - sends them into the empty-content retry loop (3 retries then failure). + Codex CLI uses reasoning-only responses as a signal that the model is still + thinking and needs another turn. This test verifies the Codex-specific path + where issuer_kind="codex_backend" preserves the old behavior. """ agent = _build_agent(monkeypatch) from agent.codex_responses_adapter import _normalize_codex_response assistant_message, finish_reason = _normalize_codex_response( - _codex_reasoning_only_response() + _codex_reasoning_only_response(), issuer_kind="codex_backend" ) assert finish_reason == "incomplete" @@ -2377,6 +2378,74 @@ def test_normalize_codex_response_marks_reasoning_only_as_incomplete(monkeypatch assert assistant_message.codex_reasoning_items[0]["encrypted_content"] == "enc_abc123" +def test_normalize_codex_response_reasoning_only_completed_is_stop_for_other_backends(monkeypatch): + """Reasoning-only with status='completed' should be 'stop' for non-Codex backends. + + When response.status == "completed" and no items are queued/in_progress, + reasoning alone is a valid final state for non-Codex backends. Forcing + "incomplete" here causes multi-minute stalls (3 retries x up to 240s each). + See https://github.com/NousResearch/hermes-agent/issues/64434 + """ + agent = _build_agent(monkeypatch) + from agent.codex_responses_adapter import _normalize_codex_response + response = _codex_reasoning_only_response() + assistant_message, finish_reason = _normalize_codex_response( + response, issuer_kind="other:example-relay" + ) + + assert finish_reason == "stop" + assert assistant_message.content == "" + assert assistant_message.codex_reasoning_items is not None + assert len(assistant_message.codex_reasoning_items) == 1 + + +def test_normalize_codex_response_reasoning_only_completed_is_stop_without_issuer(monkeypatch): + """Default issuer (None) should also trust response.status='completed' for reasoning-only. + + When no issuer_kind is provided (test or default scenario) and the provider + says status='completed', reasoning-only should be treated as 'stop'. + """ + agent = _build_agent(monkeypatch) + from agent.codex_responses_adapter import _normalize_codex_response + response = _codex_reasoning_only_response() + assistant_message, finish_reason = _normalize_codex_response(response) + + assert finish_reason == "stop" + assert assistant_message.content == "" + + +def test_normalize_codex_response_reasoning_only_stays_incomplete_for_xai_backend(monkeypatch): + """xAI backend also preserves incomplete for reasoning-only (same as Codex).""" + agent = _build_agent(monkeypatch) + from agent.codex_responses_adapter import _normalize_codex_response + response = _codex_reasoning_only_response() + assistant_message, finish_reason = _normalize_codex_response( + response, issuer_kind="xai_responses" + ) + + assert finish_reason == "incomplete" + assert assistant_message.content == "" + + +def test_normalize_codex_response_reasoning_only_stays_incomplete_for_github_backend(monkeypatch): + """GitHub/Copilot Responses backend preserves incomplete for reasoning-only. + + Copilot fronts the same OpenAI model family as codex_backend and exhibits + the same reasoning-only "still thinking" degeneration mode, so it must + stay on the continuation path — only unrecognized (other:*) backends + trust response.status='completed' as terminal. + """ + agent = _build_agent(monkeypatch) + from agent.codex_responses_adapter import _normalize_codex_response + response = _codex_reasoning_only_response() + assistant_message, finish_reason = _normalize_codex_response( + response, issuer_kind="github_responses" + ) + + assert finish_reason == "incomplete" + assert assistant_message.content == "" + + def test_normalize_codex_response_reasoning_with_content_is_stop(monkeypatch): """If a response has both reasoning and message content, it should still be 'stop'.""" agent = _build_agent(monkeypatch)
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#64449 fix: finish_reason misclassified as incomplete for codex_responses (#64434) · merged via another change →
e14468d,#64764
Closed issues · 1
- Closed#64434finish_reason misclassified as "incomplete" for reasoning-only completed turns on api_mode=codex_responses (causes multi-minute stalls / iteration budget exhaustion)
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #64764
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 15 Jul 2026