{"repo":"NousResearch/hermes-agent","public_id":"64434","display_label":"Codex Responses finish_reason misclassified as incomplete for reasoning-only turns","summary":"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.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":64764,"merged_at":"2026-07-15T07:12:41+00:00"},"resolution":{"state":"resolved","by":"pr-64764"},"freshness":{"state":"verified","last_verified_at":"2026-07-15T07:12:41+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":64434,"pr_number":64764,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-15T07:12:41+00:00"}],"members":[{"kind":"issue","number":64434,"title":"finish_reason misclassified as \"incomplete\" for reasoning-only completed turns on api_mode=codex_responses (causes multi-minute stalls / iteration budget exhaustion)","url":null,"state":"closed"},{"kind":"pr","number":64449,"title":"fix: finish_reason misclassified as incomplete for codex_responses (#64434)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"e14468dde","number":0},{"kind":"pr","value":"#64764","number":64764}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":64764,"title":"fix(codex): trust response.status for reasoning-only turns on unrecognized Responses backends","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/codex_responses_adapter.py","tests/agent/test_codex_responses_adapter.py","tests/run_agent/test_run_agent_codex_responses.py"],"fix_hunks":[{"file":"agent/codex_responses_adapter.py","hunks":["@@ -1380,12 +1380,28 @@ def _normalize_codex_response(\n         finish_reason = \"incomplete\"\n     elif (reasoning_items_raw or reasoning_parts or saw_reasoning_item) and not final_text:\n         # Response contains only reasoning (encrypted thinking state and/or\n-        # human-readable summary) with no visible content or tool calls. The\n-        # model is still thinking and needs another turn to produce the actual\n-        # answer. Marking this as \"stop\" would send it into the empty-content\n-        # retry loop which burns retries then fails — treat it as incomplete so\n-        # the Codex continuation path handles it correctly.\n-        finish_reason = \"incomplete\"\n+        # human-readable summary) with no visible content or tool calls.\n+        #\n+        # For the specially-handled backends (Codex, xAI, GitHub/Copilot),\n+        # reasoning-only with status=\"completed\" means \"the model is still\n+        # thinking and needs another turn\" — treat it as incomplete so the\n+        # Codex continuation path retries instead of falling into the\n+        # empty-content retry loop.\n+        #\n+        # For all other backends (other:<base_url>, etc.), trust the provider's\n+        # own response.status signal. When status == \"completed\" and no items\n+        # are queued/in_progress/incomplete, reasoning alone is a valid final\n+        # state — forcing \"incomplete\" causes multi-minute stalls as the\n+        # continuation path re-issues calls (3 retries × up to 240s each).\n+        # See https://github.com/NousResearch/hermes-agent/issues/64434\n+        if response_status == \"completed\" and issuer_kind not in (\n+            \"codex_backend\",\n+            \"xai_responses\",\n+            \"github_responses\",\n+        ):\n+            finish_reason = \"stop\"\n+        else:\n+            finish_reason = \"incomplete\"\n     else:\n         finish_reason = \"stop\"\n     return assistant_message, finish_reason"]},{"file":"tests/agent/test_codex_responses_adapter.py","hunks":["@@ -51,6 +51,12 @@ def test_normalize_codex_response_drops_transient_rs_tmp_reasoning_items():\n \n \n def test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete():\n+    \"\"\"Summary-only reasoning keeps the continuation path for Codex backends.\n+\n+    Since #64434, an unrecognized issuer with ``response.status=\"completed\"``\n+    trusts the provider and returns ``stop`` — so this test pins the Codex\n+    backend explicitly, where reasoning-only still means \"still thinking\".\n+    \"\"\"\n     response = SimpleNamespace(\n         status=\"completed\",\n         output=[","@@ -63,7 +69,9 @@ def test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete():\n         ],\n     )\n \n-    assistant_message, finish_reason = _normalize_codex_response(response)\n+    assistant_message, finish_reason = _normalize_codex_response(\n+        response, issuer_kind=\"codex_backend\"\n+    )\n \n     assert finish_reason == \"incomplete\"\n     assert assistant_message.content == \"\""]},{"file":"tests/run_agent/test_run_agent_codex_responses.py","hunks":["@@ -2359,15 +2359,16 @@ def _codex_reasoning_only_response(*, encrypted_content=\"enc_abc123\", summary_te\n \n \n def test_normalize_codex_response_marks_reasoning_only_as_incomplete(monkeypatch):\n-    \"\"\"A response with only reasoning items and no content should be 'incomplete', not 'stop'.\n+    \"\"\"A response with only reasoning items and no content should be 'incomplete' for Codex backends.\n \n-    Without this fix, reasoning-only responses get finish_reason='stop' which\n-    sends them into the empty-content retry loop (3 retries then failure).\n+    Codex CLI uses reasoning-only responses as a signal that the model is still\n+    thinking and needs another turn. This test verifies the Codex-specific path\n+    where issuer_kind=\"codex_backend\" preserves the old behavior.\n     \"\"\"\n     agent = _build_agent(monkeypatch)\n     from agent.codex_responses_adapter import _normalize_codex_response\n     assistant_message, finish_reason = _normalize_codex_response(\n-        _codex_reasoning_only_response()\n+        _codex_reasoning_only_response(), issuer_kind=\"codex_backend\"\n     )\n \n     assert finish_reason == \"incomplete\"","@@ -2377,6 +2378,74 @@ def test_normalize_codex_response_marks_reasoning_only_as_incomplete(monkeypatch\n     assert assistant_message.codex_reasoning_items[0][\"encrypted_content\"] == \"enc_abc123\"\n \n \n+def test_normalize_codex_response_reasoning_only_completed_is_stop_for_other_backends(monkeypatch):\n+    \"\"\"Reasoning-only with status='completed' should be 'stop' for non-Codex backends.\n+\n+    When response.status == \"completed\" and no items are queued/in_progress,\n+    reasoning alone is a valid final state for non-Codex backends. Forcing\n+    \"incomplete\" here causes multi-minute stalls (3 retries x up to 240s each).\n+    See https://github.com/NousResearch/hermes-agent/issues/64434\n+    \"\"\"\n+    agent = _build_agent(monkeypatch)\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+    response = _codex_reasoning_only_response()\n+    assistant_message, finish_reason = _normalize_codex_response(\n+        response, issuer_kind=\"other:example-relay\"\n+    )\n+\n+    assert finish_reason == \"stop\"\n+    assert assistant_message.content == \"\"\n+    assert assistant_message.codex_reasoning_items is not None\n+    assert len(assistant_message.codex_reasoning_items) == 1\n+\n+\n+def test_normalize_codex_response_reasoning_only_completed_is_stop_without_issuer(monkeypatch):\n+    \"\"\"Default issuer (None) should also trust response.status='completed' for reasoning-only.\n+\n+    When no issuer_kind is provided (test or default scenario) and the provider\n+    says status='completed', reasoning-only should be treated as 'stop'.\n+    \"\"\"\n+    agent = _build_agent(monkeypatch)\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+    response = _codex_reasoning_only_response()\n+    assistant_message, finish_reason = _normalize_codex_response(response)\n+\n+    assert finish_reason == \"stop\"\n+    assert assistant_message.content == \"\"\n+\n+\n+def test_normalize_codex_response_reasoning_only_stays_incomplete_for_xai_backend(monkeypatch):\n+    \"\"\"xAI backend also preserves incomplete for reasoning-only (same as Codex).\"\"\"\n+    agent = _build_agent(monkeypatch)\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+    response = _codex_reasoning_only_response()\n+    assistant_message, finish_reason = _normalize_codex_response(\n+        response, issuer_kind=\"xai_responses\"\n+    )\n+\n+    assert finish_reason == \"incomplete\"\n+    assert assistant_message.content == \"\"\n+\n+\n+def test_normalize_codex_response_reasoning_only_stays_incomplete_for_github_backend(monkeypatch):\n+    \"\"\"GitHub/Copilot Responses backend preserves incomplete for reasoning-only.\n+\n+    Copilot fronts the same OpenAI model family as codex_backend and exhibits\n+    the same reasoning-only \"still thinking\" degeneration mode, so it must\n+    stay on the continuation path — only unrecognized (other:*) backends\n+    trust response.status='completed' as terminal.\n+    \"\"\"\n+    agent = _build_agent(monkeypatch)\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+    response = _codex_reasoning_only_response()\n+    assistant_message, finish_reason = _normalize_codex_response(\n+        response, issuer_kind=\"github_responses\"\n+    )\n+\n+    assert finish_reason == \"incomplete\"\n+    assert assistant_message.content == \"\"\n+\n+\n def test_normalize_codex_response_reasoning_with_content_is_stop(monkeypatch):\n     \"\"\"If a response has both reasoning and message content, it should still be 'stop'.\"\"\"\n     agent = _build_agent(monkeypatch)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}