Skip to content

fix(codex): retry empty buffered completions - #71

Merged
raine merged 1 commit into
raine:mainfrom
Aotricx:agent/retry-empty-buffered-completion
Jul 28, 2026
Merged

fix(codex): retry empty buffered completions#71
raine merged 1 commit into
raine:mainfrom
Aotricx:agent/retry-empty-buffered-completion

Conversation

@Aotricx

@Aotricx Aotricx commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • detect buffered upstream bodies that end in a successful terminal event (response.completed / response.done) with no semantic output, using the existing reducer (text, thinking, tool, and web-search events all count as semantic)
  • retry with previous_response_id dropped so the resend carries full context, bounded at 1 + 10 attempts
  • return an explicit 503 (Codex completed without producing output) after exhaustion instead of a successful empty end_turn
  • response.incomplete stays non-retryable (a max-tokens truncation would not improve on resend)

Failure mode

The buffered transport path — CCP_CODEX_TRANSPORT=http, and non-streaming client requests generally — translated a terminal-only upstream body into HTTP 200 with "content": [] and stop_reason: "end_turn". Same class of failure as the live-stream path fixed in #70, on the other transport.

TDD evidence

RED (all three smoke tests before the fix):

assertion `left == right` failed: exhausted empty completions must surface an explicit error:
{"content":[],"id":"msg_…","stop_reason":"end_turn",…}
  left: 200
 right: 503
test result: FAILED. 0 passed; 3 failed

GREEN:

smoke_codex_http_retries_empty_completion ... ok
smoke_codex_http_stream_retries_empty_completion ... ok
smoke_codex_http_empty_completions_exhaust_to_service_unavailable ... ok

The detection helper was also grown test-first: six unit tests (terminal-only completed/done → empty; text / tool-call / incomplete-only / no-terminal → not empty) written against a stub and watched fail before implementation.

Relationship to #70

Independent branch from main; complementary to #70 (live WebSocket path). Two hunks are intentionally identical to #70 so a later merge dedupes trivially: the EMPTY_CODEX_COMPLETION_DETAIL 503 mapping branch, and the zero-delay retry test hook in retry.rs (same *_for_tests pattern as the websocket pool/continuation helpers).

Verification

  • cargo fmt --all -- --check
  • cargo test — 604 passed, 0 failed across unit and integration suites
  • exhaustion smoke test asserts exactly 11 upstream attempts, keeping the loop provably bounded

🤖 Generated with Claude Code

Buffered Codex transports translated successful terminal events with no
semantic output into empty HTTP 200 end_turn responses. Retry these responses
instead so callers receive a usable completion or an explicit failure.

Use reducer events to distinguish output text, thinking, tool calls, and web
searches from terminal-only bodies. Message item framing without a text delta
remains empty because the accumulator omits such blocks.

Drop previous_response_id before each retry so WebSocket requests resend full
context. Bound retries to ten after the initial attempt and return 503 after
exhaustion. Abort continuation and pending compaction state when processing
fails.

Tests remove retry delays and cover buffered streaming, non-streaming,
exhaustion, and empty message scaffolding. This changes empty successful
completions from HTTP 200 end_turn responses to bounded retries and a 503.
@raine
raine force-pushed the agent/retry-empty-buffered-completion branch from 7b932b4 to 1d9d216 Compare July 28, 2026 14:12
@raine
raine merged commit dd89062 into raine:main Jul 28, 2026
1 check passed
dmitryanchikov added a commit to dmitryanchikov/claude-code-proxy that referenced this pull request Aug 26, 2026
…_result tail as a valid end_turn

When a conversation ends assistant:tool_use -> user:tool_result and the tool
call itself delivered the user-visible answer (a messaging/reply tool), the
model legitimately ends the follow-up turn with zero output items. The
empty-completion protection (raine#70/raine#71) treated this as a failure: ~11 retries,
~3 minutes, then 503 'Codex completed without producing output' — and the
Anthropic client retries on top. For a channel bot whose reply tool IS the
answer, ~68% of real turns failed this way (measured 2026-08-25); a
3-message repro is deterministic: 503 @ 175s before, 200 @ 1.7s after.

Scope is deliberately narrow, in both dimensions:
- The tail predicate runs on the ORIGINAL Anthropic body and requires a
  tool_result WITHOUT is_error — an error result demands a model reaction,
  so a deterministic empty there stays a loud failure.
- A qualifying tail alone is not proof: an internal tool (Read/Bash) has the
  same tail shape and can meet a transient upstream glitch. One retry is the
  discriminator — a glitch clears on resend, a semantic empty repeats — so
  the empty is accepted only on the SECOND consecutive occurrence, on both
  the buffered and live-stream paths (shared accept_empty_completion()).
Empty completions without a qualifying tail keep the full retry-then-503
behavior unchanged.

Tests: tail predicate (success/error/non-tool/ordering/empty), the accept
truth table, and live-path behavioral pair (no flag -> Retry with the
empty-completion detail; flag -> delivered stream ending in message_stop).
bp0lr pushed a commit to bp0lr/claude-code-proxy that referenced this pull request Aug 29, 2026
Buffered Codex transports translated successful terminal events with no
semantic output into empty HTTP 200 end_turn responses. Retry these responses
instead so callers receive a usable completion or an explicit failure.

Use reducer events to distinguish output text, thinking, tool calls, and web
searches from terminal-only bodies. Message item framing without a text delta
remains empty because the accumulator omits such blocks.

Drop previous_response_id before each retry so WebSocket requests resend full
context. Bound retries to ten after the initial attempt and return 503 after
exhaustion. Abort continuation and pending compaction state when processing
fails.

Tests remove retry delays and cover buffered streaming, non-streaming,
exhaustion, and empty message scaffolding. This changes empty successful
completions from HTTP 200 end_turn responses to bounded retries and a 503.

Co-authored-by: Hunter Moore <hmoore@silvey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants