Skip to content

Retry failed turns automatically on recoverable errors - #45

Merged
sam2tom merged 1 commit into
betafrom
codex/core-simple-turn-retry-beta
Oct 16, 2026
Merged

sam2tom merged 1 commit into
betafrom
codex/core-simple-turn-retry-beta

Conversation

@sam2tom

@sam2tom sam2tom commented Oct 16, 2026

Copy link
Copy Markdown

Summary

When a turn fails with a recoverable error (upstream 400 format problems, tool or parameter errors, transient 5xx/429/connection errors), Core now submits a follow-up message so the conversation continues instead of stopping. If the same failure repeats, the retry message adds progressively stronger guidance so the model can adjust its tool call or parameters.

This replaces the earlier daemon-side "rebuild the thread inside the same turn" approach (#42, now draft). It stays inside services/core: no daemon, protocol, delivery.go, finishRun or state machine changes.

Behaviour

  • Hook: after a worker finishes a claim or environment input, maybeAutoRetry inspects a failed turn.
  • Retried only when error_code=engine_failed and engine_error_code is one of: empty, invalid_request, server_error, server_overloaded, rate_limit_exceeded, request_timeout, connection_failed. HTTP 401/402/403/404 on connection_failed is not retried. Auth, billing, quota, model-not-found, context-too-long, policy, cancel, disconnect and input_* failures are never retried.
  • Text fingerprints that are retried: missing messages, request ending with a model turn, tool argument errors (these add a parameter hint). Encrypted reasoning, image-unsupported and tool_result image failures are not retried because the bad content already lives in the daemon's native history and Core cannot edit it.
  • Backoff 5s / 15s / 30s, in-process timer that does not hold a worker slot. Before submitting, Core re-checks that the session still exists, the failed turn is still the latest turn, and no cancel was requested. Idempotency key is auto-retry:<failed turn id>.
  • Count is read from the retry message prefix [auto-retry N/M], so no new storage. Escalation: N=1 "please continue" (plus a parameter hint for tool-argument errors), N=2 "try a different approach", N=3 "stop calling the failing tool".
  • The retry message is a normal user message, so it is visible in the conversation. Logs and messages carry only the fingerprint name and engine code, never raw error text or request bodies.

Configuration

OAC_TURN_AUTO_RETRIES: integer 0..5, default 0 (off). Invalid values fail startup. Registered as setting core.turn_auto_retries and wired through compose, the Kubernetes README, deploy.yml and both configuration docs. Roll back by setting it to 0 and restarting Core; there is no migration.

Testing

Passing: make build-core, go vet (execution, processconfig), processconfig tests, 9 new retry tests, python3 deploy/compose/test_compose.py.

Not verified:

  • Database integration tests were skipped (no OAC_TEST_DATABASE_URL locally).
  • golangci-lint was not run.
  • End to end on a real database: that a follow-up message continues the original conversation, and that the hint actually lets the model fix its arguments. Needs one real failure on beta.
  • TestSchedulingObservationBoundedConcurrentAndOwnerLocal flakes on plain origin/beta too (1 of 3 runs). The other failing packages (cmd/sandbox-node, internal/sandbox/docker, internal/sandbox/microsandbox) fail identically on origin/beta.

Rollout

  1. Merge to beta, deploy with the switch off.
  2. Set OAC_TURN_AUTO_RETRIES=3 on beta, trigger a real failure, confirm [auto-retry 1/3] appears and the conversation continues.
  3. Watch the decision and fingerprint log fields and token usage before deciding on wider rollout.

Known limits and follow-ups

  • Empty error codes are retried generically; on the claude-sdk path the upstream text is not available, so this can burn tokens on deterministic failures. Revisit after beta data.
  • Retries are in-process, so a Core restart drops pending retries (the user can resend).
  • The count is read from the first message input; a user message merged into the same turn can reset it.
  • Image and encrypted-reasoning failures need the daemon to trim native history or forward sanitized upstream error text. That would be a small separate change.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T04:35:56.212920Z 87b13e4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87b13e456d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

map[string]any{"type": "input_text", "text": retryMessage(attempt+1, limit, decision)},
}}},
})
if _, err := host.SubmitInputs(submit, tenant, turn.SessionID, "auto-retry:"+turn.ID, []sessions.Input{{Kind: "message", Payload: payload}}); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep automatic retries out of fabricated user history

When OAC_TURN_AUTO_RETRIES is enabled and an eligible failure occurs, this persists Core's control prompt through the normal input path as a role: user message. It therefore creates a public user Item and root Turn, changes Session history and status despite no application request, and provides no provenance by which clients can distinguish the synthetic input. The Sessions contract defines these messages as client-admitted input, so the retry needs a typed representation below the public input boundary rather than inventing new public semantics.

AGENTS.md reference: AGENTS.md:L49-L50

Useful? React with 👍 / 👎.

map[string]any{"type": "input_text", "text": retryMessage(attempt+1, limit, decision)},
}}},
})
if _, err := host.SubmitInputs(submit, tenant, turn.SessionID, "auto-retry:"+turn.ID, []sessions.Input{{Kind: "message", Payload: payload}}); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard retry admission atomically against newer input

If an application submits a follow-up after ListTurns returns but before this call acquires the Session lock, the latest-Turn check passes even though the application has now created a new queued or running Turn. SubmitInputs then treats the stale retry message as steering for that unrelated Turn—or starts another Turn if the follow-up already completed—so a user request receives an internal instruction meant for the preceding failure. The failed-Turn/latest-state precondition must be checked in the same transaction that admits the retry.

Useful? React with 👍 / 👎.

@sam2tom
sam2tom merged commit 67308c4 into beta Oct 16, 2026
48 of 50 checks 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.

1 participant