Skip to content

fix(codex): rescue reasoning-only turns that die with 'remained incomplete after 3 continuation attempts' - #63690

Closed
IpastorSan wants to merge 1 commit into
NousResearch:mainfrom
IpastorSan:fix/xai-reasoning-only-incomplete
Closed

IpastorSan wants to merge 1 commit into
NousResearch:mainfrom
IpastorSan:fix/xai-reasoning-only-incomplete

Conversation

@IpastorSan

@IpastorSan IpastorSan commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Problem

On the xAI /v1/responses surface (xai-oauth), grok-4.x sometimes ends a turn with only reasoning items — no message output item, no tool calls. When that happens, the turn deterministically dies with:

Codex response remained incomplete after 3 continuation attempts

Two compounding causes:

  1. The answer can land in the reasoning channel. The model occasionally emits its final answer inside the reasoning item, delimited by grok's internal <response> tag. The answer exists, but _normalize_codex_response classifies the turn as reasoning-only → finish_reason="incomplete".

  2. Continuations are a no-op. grok's reasoning items on this surface carry no encrypted_content, so the interim assistant message (plain-text reasoning only, no content, no replayable items) emits nothing in _chat_messages_to_responses_input. Every continuation request is therefore byte-identical to the one that just came back incomplete — the model repeats the reasoning-only response until the retry budget is exhausted.

Observed live with grok-4.20 on xai-oauth (2026-07): the failure pattern shows up as triplets of finish_reason="incomplete" assistant messages with populated reasoning and empty content, across multiple unrelated sessions in ordinary usage. This is a sibling of the grok-composer web_search incomplete-loop fix already documented in transports/codex.py.

Related issues

Fix

  • _normalize_codex_response (scoped to issuer_kind == "xai_responses"): when a turn has no visible content and no tool calls but the reasoning text contains grok's <response> delimiter, promote the delimited tail to assistant content (the untagged prefix stays as thinking text). The turn completes normally on the first attempt.
  • Codex-incomplete continuation path (conversation_loop.py): when the interim message has nothing the input converter will replay (no content, no encrypted reasoning items, no message items), append a user-role nudge so the retry actually differs and explicitly asks for the final answer / pending tool call. Mirrors the existing _get_continuation_prompt pattern used for length truncation, and is guarded against duplicate insertion. Turns whose interim is replayable (visible content, encrypted reasoning) are untouched.

Tests

  • 4 new tests in tests/agent/test_codex_responses_adapter.py: salvage with/without closing tag, xAI-only scoping, no-marker case stays incomplete.
  • 2 new tests in tests/run_agent/test_run_agent_codex_responses.py: nudge is appended exactly once after an unreplayable reasoning-only interim (and the turn then completes); nudge does NOT fire for a replayable interim.
  • scripts/run_tests.sh tests/agent/test_codex_responses_adapter.py tests/run_agent/test_run_agent_codex_responses.py (hermetic CI wrapper): 114 passed, 0 failed.
  • scripts/check-windows-footguns.py --diff: clean (change is pure string/dict handling, no OS-specific code).

Platforms tested

  • Linux (Arch, Python 3.11 via uv). No file I/O, process, or terminal handling touched, so no platform-specific surface beyond the test suite.

🤖 Generated with Claude Code

…plete after 3 continuation attempts'

grok-4.x on the xAI /v1/responses surface sometimes ends a turn with only
reasoning items — no message output item, no tool calls — and those
reasoning items carry no encrypted_content. Two compounding problems:

1. The model occasionally emits its final answer INSIDE the reasoning
   channel, delimited by grok's internal "<response>" tag. The answer
   exists but is classified reasoning-only → finish_reason=incomplete.

2. An interim assistant message holding only plain-text reasoning replays
   as nothing in _chat_messages_to_responses_input, so every continuation
   request is byte-identical to the one that just failed. The model
   deterministically repeats the reasoning-only response until the retry
   budget is exhausted and the turn dies with "Codex response remained
   incomplete after 3 continuation attempts".

Fixes:
- _normalize_codex_response (xai_responses only): salvage the
  <response>-delimited tail from the reasoning text and promote it to
  assistant content; the untagged prefix stays as thinking text.
- Codex-incomplete continuation path: when the interim message has
  nothing the input converter will replay (no content, no encrypted
  reasoning items, no message items), append a user-role nudge so the
  retry actually differs and explicitly asks for the final answer /
  pending tool call. Mirrors the existing _get_continuation_prompt
  pattern used for length truncation.

Observed live with grok-4.20 on xai-oauth (2026-07-13); sibling of the
grok-composer web_search incomplete-loop fix in transports/codex.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/xai xAI (Grok) codex labels Jul 13, 2026
teknium1 added a commit that referenced this pull request Jul 15, 2026
…lations

Follow-up to the salvaged #63690: when the interim assistant message is
too empty to append (no content and no reasoning of any kind), the last
message in history is still the prior user/tool turn — appending the
user-role nudge there would create a user→user or tool→user sequence
that strict providers reject. Only append the nudge when the last
message is an assistant turn. Also add IpastorSan to AUTHOR_MAP.
@teknium1

Copy link
Copy Markdown
Collaborator

Merged via PR #64768 — your commit was cherry-picked onto current main with your authorship preserved in git log (rebase merge). We added a small follow-up: an alternation guard on the continuation nudge so an interim turn too empty to append can never produce a user→user / tool→user sequence on strict providers. Excellent PR — the two-cause analysis (answer in the reasoning channel + byte-identical retries from unreplayable interims) and the live grok-4.20 observations made this a straightforward salvage.

@teknium1 teknium1 closed this Jul 15, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…lations

Follow-up to the salvaged NousResearch#63690: when the interim assistant message is
too empty to append (no content and no reasoning of any kind), the last
message in history is still the prior user/tool turn — appending the
user-role nudge there would create a user→user or tool→user sequence
that strict providers reject. Only append the nudge when the last
message is an assistant turn. Also add IpastorSan to AUTHOR_MAP.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…lations

Follow-up to the salvaged NousResearch#63690: when the interim assistant message is
too empty to append (no content and no reasoning of any kind), the last
message in history is still the prior user/tool turn — appending the
user-role nudge there would create a user→user or tool→user sequence
that strict providers reject. Only append the nudge when the last
message is an assistant turn. Also add IpastorSan to AUTHOR_MAP.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…lations

Follow-up to the salvaged NousResearch#63690: when the interim assistant message is
too empty to append (no content and no reasoning of any kind), the last
message in history is still the prior user/tool turn — appending the
user-role nudge there would create a user→user or tool→user sequence
that strict providers reject. Only append the nudge when the last
message is an assistant turn. Also add IpastorSan to AUTHOR_MAP.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…lations

Follow-up to the salvaged NousResearch#63690: when the interim assistant message is
too empty to append (no content and no reasoning of any kind), the last
message in history is still the prior user/tool turn — appending the
user-role nudge there would create a user→user or tool→user sequence
that strict providers reject. Only append the nudge when the last
message is an assistant turn. Also add IpastorSan to AUTHOR_MAP.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/xai xAI (Grok) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants