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