Skip to content

fix(codex): keep shared network failures account-neutral - #922

Open
luvs01 wants to merge 3 commits into
lidge-jun:devfrom
luvs01:fix/914-account-neutral-network
Open

fix(codex): keep shared network failures account-neutral#922
luvs01 wants to merge 3 commits into
lidge-jun:devfrom
luvs01:fix/914-account-neutral-network

Conversation

@luvs01

@luvs01 luvs01 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • classify only exact pre-connect DNS and network-reachability failures as account-neutral
  • preserve the existing 502 response while leaving failure streaks, cooldowns, thread affinity, and active-account selection unchanged
  • release only a quota-probe lease owned by the interrupted request
  • keep timeout, abort, reset, socket-close, malformed error, HTTP 5xx, and semantic upstream failures on their existing account-scoped paths
  • document the account-scoped meaning of upstreamFailoverThreshold in every shipped documentation locale

Root cause

Regular Responses and native compact mapped every non-timeout transport exception to an account-specific connect_error. A host-wide DNS outage therefore incremented each selected credential's failure streak and could clear affinity or rotate accounts even though every account used the same unreachable ChatGPT host.

The classifier is deliberately narrow: it trusts exact own-data code and cause properties through a bounded chain, and any abort, timeout, reset, or socket-close blocker wins.

User impact

An existing Codex task remains on its affined account during shared DNS or reachability failures. The request still receives a retryable 502, but OpenCodex no longer treats the outage as evidence that another credential is healthier.

This does not retry a partially committed request, suppress real upstream rejections, or change quota-based selection for new/unbound work.

Validation

  • bun test tests/codex-account-neutral-network.test.ts
    • 6 passed, 0 failed, 80 assertions on Bun 1.4.0-canary.1
  • bun test tests/server-auth.test.ts --timeout 15000
    • 58 passed, 0 failed, 357 assertions on Bun 1.4.0-canary.1
  • bun run typecheck
    • passed
  • bun run privacy:scan
    • passed
  • git diff --check origin/dev...HEAD
    • passed
  • node_modules/bun/bin/bun install --frozen-lockfile && node_modules/bun/bin/bun run build from docs-site/
    • passed with the repository-pinned Bun 1.3.14; 216 pages built

A prior local full-suite run exercised 7,442 tests. Its remaining Windows/runtime-provenance, fixed-timeout, and local dependency failures were reproduced or isolated on clean origin/dev; the one stale expectation affected by this change was corrected and the complete server-auth.test.ts suite now passes. Hosted CI on the final dev base remains required.

Review gates

This touches authentication/account-routing behavior. Per the current contribution policy it requires the maintainer-sponsored label and explicit maintainer security review before merge. The PR is otherwise complete and ready for review.

Fixes #914.

Related: #919 covers a distinct post-header mid-stream body failure; this PR handles only pre-header DNS and network reachability failures.

Summary by CodeRabbit

  • Bug Fixes

    • Improved upstream failover handling for account-neutral DNS and network reachability failures.
    • These failures now return a 502 response without penalizing, rotating, or recording health against the selected account.
    • Probe leases are correctly released while account affinity is preserved.
    • Abort, timeout, reset, ambiguous, and malformed errors continue to be handled safely.
  • Documentation

    • Clarified upstreamFailoverThreshold behavior across supported languages, including which failures count and how to disable the threshold.

@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds bounded classification for account-neutral DNS and network reachability failures. Compact and regular Codex Responses flows preserve account health and affinity, release owned probe leases, and return 502. Provider documentation now describes the updated failover threshold behavior in five locales.

Account-neutral network failure handling

Layer / File(s) Summary
Bounded network error classification
src/lib/upstream-retry.ts
Adds exact-code classification through safe, bounded cause-chain traversal. It rejects cycles, unsafe properties, timeouts, aborts, resets, connection-scoped failures, and ambiguous messages.
Codex response and lease handling
src/server/responses/compact.ts, src/server/responses/core.ts, tests/codex-account-neutral-network.test.ts, tests/server-auth.test.ts
Neutral failures release Codex probe leases without recording account health or changing affinity. Tests cover regular and compact flows, concurrent failures, routing preservation, lease release, and pre-connect reachability failures.
Failover threshold documentation
docs-site/src/content/docs/{,ja/,ko/,ru/,zh-cn/}reference/configuration/providers.md
Documents that host-wide DNS and network reachability failures do not count toward upstreamFailoverThreshold.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Codex client
  participant Responses flow
  participant Account pool
  participant Probe lease
  Codex client->>Responses flow: Send request
  Responses flow->>Account pool: Classify transport failure
  Account pool-->>Responses flow: Preserve health and affinity
  Responses flow->>Probe lease: Release owned lease
  Responses flow-->>Codex client: Return 502
Loading

Possibly related PRs

Suggested reviewers: lidge-jun, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: keeping shared Codex network failures neutral to account selection.
Linked Issues check ✅ Passed The implementation, tests, and documentation address issue #914, including bounded code classification, state preservation, lease release, and regular and compact paths.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #914 and cover implementation, regression tests, and localized documentation for the requested behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@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: 888c9558b3

ℹ️ 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".

| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Assignment strategy for new/unbound Codex requests. A request is unbound when it has no live (parent thread id, quota scope) affinity; a visible existing task can become unbound after proxy restart or affinity reset. `quota` picks the lowest-usage eligible account when no active account exists, keeps an eligible active account below `autoSwitchThreshold`, and after the threshold may move an unbound request or proactively rebind a bound task to a lower-usage eligible account. `round-robin` distributes unbound requests evenly; `fill-first` keeps assigning unbound requests to the active account until cooldown, unavailability, or the configured drain threshold. |
| `accountPoolStickyLimit?` | `number` | `1` | New/unbound task assignments retained on one round-robin selection before advancing; the counter advances when a task is bound, not after an upstream success. Range 1–100. |
| `upstreamFailoverThreshold?` | `number` | `3` | Consecutive transient failures before future new sessions fail over. Set `0` to disable. |
| `upstreamFailoverThreshold?` | `number` | `3` | Consecutive account-scoped transient outcomes before future new sessions fail over. Host-wide DNS and network reachability failures are account-neutral and do not count. Set `0` to disable. |

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 Keep pool-backed sidecar failures account-neutral

When a routed request uses a pool-backed OpenAI web-search or vision sidecar, a DNS/reachability exception still reaches recordOutcome as connect_error in src/web-search/executor.ts:96-98 and src/vision/describe.ts:116-118; the callback in src/providers/openai-sidecar.ts:100-108 then increments that account's failure streak and can rotate it. Thus the documented guarantee is false for sidecar-enabled requests. Apply the new classifier and probe-lease handling to these sidecar catches as well, or narrow this claim.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/reference/configuration/providers.md`:
- Line 25: Update the upstreamFailoverThreshold documentation to explicitly
describe account-neutral failures as runtime-classified exact pre-connect DNS
and reachability errors within a bounded cause chain, and state that timeouts,
aborts, resets, socket closures, HTTP failures, and semantic upstream failures
remain account-scoped. Apply equivalent wording in
docs-site/src/content/docs/reference/configuration/providers.md lines 25-25,
docs-site/src/content/docs/ja/reference/configuration/providers.md lines 24-24,
docs-site/src/content/docs/ko/reference/configuration/providers.md lines 24-24,
docs-site/src/content/docs/ru/reference/configuration/providers.md lines 25-25,
and docs-site/src/content/docs/zh-cn/reference/configuration/providers.md lines
24-24, translating the qualifiers appropriately for each locale.

In `@src/lib/upstream-retry.ts`:
- Around line 32-53: Update the ACCOUNT_NEUTRAL_NETWORK_ERROR_CODES and
ACCOUNT_SCOPED_CONNECTION_ERROR_CODES sets to match Bun 1.3.14: retain ENOTFOUND
and the existing supported codes, and remove DNSResolveFailed,
DNSResolutionFailed, Timeout, Aborted, AbortedBeforeConnecting, and
ClientAborted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8aa7417e-a90c-4725-b3f6-750bc7b24b6d

📥 Commits

Reviewing files that changed from the base of the PR and between fa51fce and 888c955.

📒 Files selected for processing (10)
  • docs-site/src/content/docs/ja/reference/configuration/providers.md
  • docs-site/src/content/docs/ko/reference/configuration/providers.md
  • docs-site/src/content/docs/reference/configuration/providers.md
  • docs-site/src/content/docs/ru/reference/configuration/providers.md
  • docs-site/src/content/docs/zh-cn/reference/configuration/providers.md
  • src/lib/upstream-retry.ts
  • src/server/responses/compact.ts
  • src/server/responses/core.ts
  • tests/codex-account-neutral-network.test.ts
  • tests/server-auth.test.ts

| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Assignment strategy for new/unbound Codex requests. A request is unbound when it has no live (parent thread id, quota scope) affinity; a visible existing task can become unbound after proxy restart or affinity reset. `quota` picks the lowest-usage eligible account when no active account exists, keeps an eligible active account below `autoSwitchThreshold`, and after the threshold may move an unbound request or proactively rebind a bound task to a lower-usage eligible account. `round-robin` distributes unbound requests evenly; `fill-first` keeps assigning unbound requests to the active account until cooldown, unavailability, or the configured drain threshold. |
| `accountPoolStickyLimit?` | `number` | `1` | New/unbound task assignments retained on one round-robin selection before advancing; the counter advances when a task is bound, not after an upstream success. Range 1–100. |
| `upstreamFailoverThreshold?` | `number` | `3` | Consecutive transient failures before future new sessions fail over. Set `0` to disable. |
| `upstreamFailoverThreshold?` | `number` | `3` | Consecutive account-scoped transient outcomes before future new sessions fail over. Host-wide DNS and network reachability failures are account-neutral and do not count. Set `0` to disable. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the neutral-failure boundary explicit in every locale.

The five lines broaden the account-neutral category. The runtime applies it only to exact pre-connect DNS and reachability errors found within a bounded cause chain. Timeouts, aborts, resets, socket closures, HTTP failures, and semantic upstream failures remain account-scoped.

  • docs-site/src/content/docs/reference/configuration/providers.md#L25-L25: update the canonical English wording with “pre-connect” and “runtime-classified exact errors,” then state that other failures remain account-scoped.
  • docs-site/src/content/docs/ja/reference/configuration/providers.md#L24-L24: translate the same qualifiers into Japanese.
  • docs-site/src/content/docs/ko/reference/configuration/providers.md#L24-L24: translate the same qualifiers into Korean.
  • docs-site/src/content/docs/ru/reference/configuration/providers.md#L25-L25: translate the same qualifiers into Russian.
  • docs-site/src/content/docs/zh-cn/reference/configuration/providers.md#L24-L24: translate the same qualifiers into Simplified Chinese.

As per path instructions, keep all localized provider documentation synchronized with actual runtime behavior.

📍 Affects 5 files
  • docs-site/src/content/docs/reference/configuration/providers.md#L25-L25 (this comment)
  • docs-site/src/content/docs/ja/reference/configuration/providers.md#L24-L24
  • docs-site/src/content/docs/ko/reference/configuration/providers.md#L24-L24
  • docs-site/src/content/docs/ru/reference/configuration/providers.md#L25-L25
  • docs-site/src/content/docs/zh-cn/reference/configuration/providers.md#L24-L24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/configuration/providers.md` at line 25,
Update the upstreamFailoverThreshold documentation to explicitly describe
account-neutral failures as runtime-classified exact pre-connect DNS and
reachability errors within a bounded cause chain, and state that timeouts,
aborts, resets, socket closures, HTTP failures, and semantic upstream failures
remain account-scoped. Apply equivalent wording in
docs-site/src/content/docs/reference/configuration/providers.md lines 25-25,
docs-site/src/content/docs/ja/reference/configuration/providers.md lines 24-24,
docs-site/src/content/docs/ko/reference/configuration/providers.md lines 24-24,
docs-site/src/content/docs/ru/reference/configuration/providers.md lines 25-25,
and docs-site/src/content/docs/zh-cn/reference/configuration/providers.md lines
24-24, translating the qualifiers appropriately for each locale.

Source: Path instructions

Comment thread src/lib/upstream-retry.ts
@lidge-jun

Copy link
Copy Markdown
Owner

Reviewing this against a runtime probe, because I spent four audit rounds on the same problem in a planning unit and want to hand over what killed each attempt rather than have you rediscover it.

Including ConnectionRefused is the right call and it is the thing my first design missed. Bun's fetch does not surface Node codes at all, so a classifier built only on ENOTFOUND/EAI_AGAIN is green in CI against an injected Object.assign(err, {code}) and dead in production. You caught that; I did not, until a reviewer probed the runtime.

But there is a consequence worth deciding deliberately. On Bun 1.3.14 a nonexistent hostname and a refused port are indistinguishable from the error object:

fetch("https://no-such-host.invalid/x")  -> code: "ConnectionRefused", errno: 0, no cause
fetch("http://127.0.0.1:1/x")            -> code: "ConnectionRefused", errno: 0, no cause

Same code, same errno, no cause on either. So ConnectionRefused in the neutral set means a genuinely refused connection to a resolving upstream host also stops counting against account health. That may well be what you want — every pool account shares the host, so rotating cannot repair it either — but the PR describes the set as "exact pre-connect DNS and network-reachability failures", and on Bun it is broader than that. Worth saying so in the code comment so the next reader does not assume the distinction holds.

Two cases my audit found that are worth adding to your test matrix, both of which put a rejection on this path after the credential was seen:

  1. Redirects. Bun follows them by default. A server can accept the authenticated request, return 307 to a dead host, and the wrapper rejects with ConnectionRefused — headers arrived, credential evaluated.
  2. Read-then-close. A server that reads Authorization and closes the socket without responding yields ECONNRESET. Your account-scoped set covers this one correctly, but the pair is worth pinning together so a later refactor cannot move ECONNRESET across the line by accident.

One more, from the same audit: a transient 5xx that exhausts fetchWithTransientRetry() surfaces as a rejected promise rather than a response, so "the promise rejected" and "no HTTP response arrived" are not the same condition on this path. Your classifier keys on error shape rather than on rejection, so I do not think it is affected — but it is the kind of thing that bites a later simplification.

The analysis that produced all of this is in devlog/_plan/260803_transport_attribution/000_plan.md on dev, including the three designs that failed and why. Your approach is a fourth one I had not considered, and the ConnectionRefused inclusion is what makes it viable where mine were not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants