{"repo":"NousResearch/hermes-agent","public_id":"27988","display_label":"Codex Responses final_answer vs incomplete status mapping","summary":"The members concern the Codex Responses adapter's handling of a final_answer phase when the top-level response status is incomplete, including a report of finish_reason=incomplete on Azure Foundry. Two members are titled as fixes for this behavior, and the third describes the issue they appear to address.","subsystem":"agent","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":45814,"merged_at":"2026-06-13T20:45:30+00:00"},"resolution":{"state":"resolved","by":"pr-45814"},"freshness":{"state":"verified","last_verified_at":"2026-06-13T20:45:30+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":27988,"pr_number":45814,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-13T20:45:30+00:00"}],"members":[{"kind":"issue","number":27988,"title":"Codex Responses adapter maps complete final_answer to finish_reason=incomplete when top-level response.status is incomplete on Azure Foundry","url":null,"state":"closed"},{"kind":"pr","number":28039,"title":"fix(codex-responses): final_answer phase overrides top-level incomplete status (#27988)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7d11fa4e9","number":0},{"kind":"pr","value":"#45814","number":45814}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":45814,"title":"fix(codex-responses): let final_answer complete top-level incomplete responses","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/run_agent/test_run_agent_codex_responses.py"],"fix_hunks":[{"file":"agent/codex_responses_adapter.py","hunks":["@@ -1081,6 +1081,7 @@ def _normalize_codex_response(\n     message_items_raw: List[Dict[str, Any]] = []\n     tool_calls: List[Any] = []\n     has_incomplete_items = response_status in {\"queued\", \"in_progress\", \"incomplete\"}\n+    saw_streaming_or_item_incomplete = response_status in {\"queued\", \"in_progress\"}\n     saw_commentary_phase = False\n     saw_final_answer_phase = False\n     saw_reasoning_item = False","@@ -1095,6 +1096,7 @@ def _normalize_codex_response(\n \n         if item_status in {\"queued\", \"in_progress\", \"incomplete\"}:\n             has_incomplete_items = True\n+            saw_streaming_or_item_incomplete = True\n \n         if item_type == \"message\":\n             item_phase = getattr(item, \"phase\", None)","@@ -1252,7 +1254,9 @@ def _normalize_codex_response(\n         finish_reason = \"tool_calls\"\n     elif leaked_tool_call_text:\n         finish_reason = \"incomplete\"\n-    elif has_incomplete_items or (saw_commentary_phase and not saw_final_answer_phase):\n+    elif saw_streaming_or_item_incomplete:\n+        finish_reason = \"incomplete\"\n+    elif (has_incomplete_items or saw_commentary_phase) and not saw_final_answer_phase:\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"]},{"file":"tests/run_agent/test_run_agent_codex_responses.py","hunks":["@@ -154,6 +154,22 @@ def _codex_ack_message_response(text: str):\n     )\n \n \n+def _codex_final_answer_with_top_level_incomplete_response(text: str):\n+    return SimpleNamespace(\n+        output=[\n+            SimpleNamespace(\n+                type=\"message\",\n+                phase=\"final_answer\",\n+                status=\"completed\",\n+                content=[SimpleNamespace(type=\"output_text\", text=text)],\n+            )\n+        ],\n+        usage=SimpleNamespace(input_tokens=4, output_tokens=2, total_tokens=6),\n+        status=\"incomplete\",\n+        model=\"gpt-5.4\",\n+    )\n+\n+\n class _FakeCreateStream:\n     \"\"\"Iterable-only fake for ``responses.create(stream=True)`` outputs.\n ","@@ -1351,6 +1367,92 @@ def test_normalize_codex_response_marks_commentary_only_message_as_incomplete(mo\n     assert \"inspect the repository\" in (assistant_message.content or \"\")\n \n \n+def test_normalize_codex_response_final_answer_overrides_top_level_incomplete(monkeypatch):\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+\n+    assistant_message, finish_reason = _normalize_codex_response(\n+        _codex_final_answer_with_top_level_incomplete_response(\n+            \"Briefly:\\n\\n- I'm Ramsay, your assistant.\"\n+        )\n+    )\n+\n+    assert finish_reason == \"stop\"\n+    assert \"Ramsay\" in (assistant_message.content or \"\")\n+\n+\n+def test_normalize_codex_response_top_level_incomplete_without_final_answer_stays_incomplete(monkeypatch):\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+\n+    response = SimpleNamespace(\n+        output=[\n+            SimpleNamespace(\n+                type=\"message\",\n+                status=\"completed\",\n+                content=[SimpleNamespace(type=\"output_text\", text=\"Partial...\")],\n+            )\n+        ],\n+        usage=SimpleNamespace(input_tokens=4, output_tokens=2, total_tokens=6),\n+        status=\"incomplete\",\n+        model=\"gpt-5.4\",\n+    )\n+\n+    _, finish_reason = _normalize_codex_response(response)\n+\n+    assert finish_reason == \"incomplete\"\n+\n+\n[redacted:email](\"top_level_status\", [\"queued\", \"in_progress\"])\n+def test_normalize_codex_response_final_answer_does_not_override_streaming_status(\n+    monkeypatch, top_level_status\n+):\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+\n+    response = SimpleNamespace(\n+        output=[\n+            SimpleNamespace(\n+                type=\"message\",\n+                phase=\"final_answer\",\n+                status=\"completed\",\n+                content=[SimpleNamespace(type=\"output_text\", text=\"Interim answer.\")],\n+            )\n+        ],\n+        usage=SimpleNamespace(input_tokens=4, output_tokens=2, total_tokens=6),\n+        status=top_level_status,\n+        model=\"gpt-5.4\",\n+    )\n+\n+    _, finish_reason = _normalize_codex_response(response)\n+\n+    assert finish_reason == \"incomplete\"\n+\n+\n+def test_normalize_codex_response_final_answer_does_not_override_per_item_in_progress(monkeypatch):\n+    from agent.codex_responses_adapter import _normalize_codex_response\n+\n+    response = SimpleNamespace(\n+        output=[\n+            SimpleNamespace(\n+                type=\"message\",\n+                phase=\"final_answer\",\n+                status=\"completed\",\n+                content=[SimpleNamespace(type=\"output_text\", text=\"Partial final.\")],\n+            ),\n+            SimpleNamespace(\n+                type=\"message\",\n+                status=\"in_progress\",\n+                content=[SimpleNamespace(type=\"output_text\", text=\"\")],\n+            ),\n+        ],\n+        usage=SimpleNamespace(input_tokens=4, output_tokens=2, total_tokens=6),\n+        status=\"completed\",\n+        model=\"gpt-5.4\",\n+    )\n+\n+    _, finish_reason = _normalize_codex_response(response)\n+\n+    assert finish_reason == \"incomplete\"\n+\n+\n def test_normalize_codex_response_preserves_message_status_for_replay(monkeypatch):\n     \"\"\"Incomplete Codex output messages must not be replayed as completed.\"\"\"\n     agent = _build_agent(monkeypatch)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}