# NousResearch/hermes-agent · Codex Responses finish_reason misclassified as incomplete for reasoning-only turns

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #64764. Verify it addresses your case.
- **Recommended:** read PR #64764 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-64764`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #64434: finish_reason misclassified as "incomplete" for reasoning-only completed turns on api_mode=codex_responses (causes multi-minute stalls / iteration budget exhaustion)

## Prior & competing PRs

- **#64764: fix(codex): trust response.status for reasoning-only turns on unrecognized Responses backends**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/codex_responses_adapter.py`, `tests/agent/test_codex_responses_adapter.py`, `tests/run_agent/test_run_agent_codex_responses.py`
  - fix hunks:
  - `agent/codex_responses_adapter.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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=[
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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"
    ```

    ```diff
    @@ -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)
    ```
- **#64449: fix: finish_reason misclassified as incomplete for codex_responses (#64434)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `e14468dde`, `#64764`

## Fix status

- Merged to main in PR #64764 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-15`.
