Skip to content

fix(codex-pool): stabilize account health and reuse WS per thread under concurrency - #6860

Merged
lidge-jun merged 1 commit into
devfrom
cursor/codex-pool-contention-dcdd
Oct 9, 2026
Merged

lidge-jun merged 1 commit into
devfrom
cursor/codex-pool-contention-dcdd

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

  • Codex pool placement now keeps a 60-second sliding failure ratio beside the existing consecutive-failure counter. At least 20 terminal samples and a transient-failure ratio of 25% or more mark an account degraded. It clears only after the ratio stays at or below 10% for 30 seconds. The ratio does not depend on the order completions arrive.
  • A degraded account is skipped for new threads only. A thread that already has a binding stays on its account. A manual pin stays in use and the proxy logs that the pin is degraded. Set codexPinnedTransientPolicy to detour-new-threads to place only new threads elsewhere. codexFailureWindow: false restores consecutive-only steering.
  • codexWsReuseAcrossTurns (default false) retains one Codex WebSocket per account and thread across turns, waits at most 750ms for a busy socket before dialing another, keeps up to two sockets when the model or service tier changes, and leaves turn-state headers on each frame instead of the handshake. Authorization, account, proxy, or dial-URL changes still retire the old socket.
  • Upstream failures record a bounded error.code and openai-request-id / x-request-id on the request log. The log line does not include the response body. HTTP 5xx (and failures with no status yet) also print that line.

Refs #6740

Plan

The consecutive counter in src/codex/routing.ts treats interleaved successes and failures as a streak, so concurrent completions on one account can hide a high failure rate or trip failover from ordering alone. The window stores timestamped success and transient-fault samples and computes failed / samples. Quota and caller errors are not samples. Bound threads keep consulting shouldFailover, which stays consecutive-only, so the window cannot move an in-flight conversation. Unbound selection consults failureWindowSteersNewThreads.

WebSocket reuse today includes turn_id in the pool scope and copies x-codex-turn-state / x-codex-turn-metadata onto the upgrade. A busy socket returns immediately and the caller dials a throwaway. Those changes are gated by codexWsReuseAcrossTurns so the default path matches current behavior, including the plugin test that the handshake keeps the original turn-state header.

Audit

dev was still 006cec99f, the same commit the design was checked against. CODEX_FAILURE_WINDOW_MS is the 5-minute consecutive-streak gap, so the ratio window uses its own 60-second constant. shouldFailover was not widened: the affinity path uses it to detour a bound thread, which this change must not do.

src/codex/routing.ts is at its file-size cap, so the success-streak bookkeeping moved into src/codex/routing/failure-window.ts and the routing file shrank. src/server/request-log.ts and scripts/test-layout/layout.json were within a few lines of the 2000-line scan, so the diagnostic helper and the new layout keys were kept inside that budget. The reuse flag is published from warnDegradedTopLevelOptIns on a successful config load rather than from schema parsing, so a partial salvage parse cannot clear an operator opt-in.

No part of the requested scope was dropped as unsafe. The dashboard does not yet show a degraded badge; the pin warning is the proxy log line. In-flight turns were already left on their captured account, and this change does not add a resend after send.

Verification

  • bun test tests/codex-integration/codex-failure-window.test.ts tests/responses/codex-ws-pool-contention.test.ts tests/usage/upstream-failure-diagnostics.test.ts — 15 pass
  • bun test tests/codex-integration/codex-routing.test.ts tests/usage/request-log.test.ts tests/config/protocol-settings.test.ts plus the three new files — 288 pass, 1 skip, 0 fail
  • bun test tests/responses/ws-upstream-reuse.test.ts tests/lib/plugin-upstream-hooks.test.ts tests/test-layout.test.ts — pass
  • bun run typecheck — pass
  • bun run structure:check — pass
  • bun run privacy:scan — pass
  • bun scripts/file-size-ratchet.ts — pass
  • Full bun test was not run locally. The remaining coverage is CI. tests/test-layout-tooling.test.ts has one environment failure here: the VM's default Bun is 1.4.2 and that case requires the pinned 1.4.0 runner. The layout membership oracle in the same file passed.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.
Open in Web Open in Cursor 

…er concurrency

Track transient Codex failures as a 60-second ratio so concurrent completions
can steer new threads without dropping a bound thread or a manual pin.
Cross-turn WebSocket reuse stays off unless it is explicitly enabled.

Co-authored-by: JUN <jun@lidgeai.com>
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 4f9b9ee6-377c-4c43-80fa-b4aafdcd68ec

📥 Commits

Reviewing files that changed from the base of the PR and between 006cec9 and c9e49e7.


📒 Files selected for processing (27)
  • docs-site/src/content/docs/fr/reference/configuration/providers.md
  • 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/tr/reference/configuration/providers.md
  • docs-site/src/content/docs/zh-cn/reference/configuration/providers.md
  • docs-site/src/content/docs/zh-tw/reference/configuration/providers.md
  • scripts/test-layout/layout.json
  • src/codex/routing.ts
  • src/codex/routing/failure-window.ts
  • src/codex/routing/selection.ts
  • src/config/codex-ws-reuse-setting.ts
  • src/config/load-degrade.ts
  • src/config/schema/config-schema.ts
  • src/server/request-log.ts
  • src/server/responses/codex-ws-pool.ts
  • src/server/responses/passthrough-delivery.ts
  • src/server/responses/ws-upstream.ts
  • src/types/config.ts
  • structure/ops/plugins.md
  • structure/providers/openai-accounts.md
  • structure/transports/responses.md
  • tests/codex-integration/codex-failure-window.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/codex-ws-pool-contention.test.ts
  • tests/usage/upstream-failure-diagnostics.test.ts

 _________________________________________________________________________________________________________
< If a system is to serve the creative spirit, it must be entirely comprehensible to a single individual. >
 ---------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Oct 9, 2026
@lidge-jun
lidge-jun marked this pull request as ready for review October 9, 2026 23:09
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 9, 2026 23:09
@lidge-jun
lidge-jun merged commit d782adf into dev Oct 9, 2026
40 of 41 checks passed
@lidge-jun
lidge-jun deleted the cursor/codex-pool-contention-dcdd branch October 9, 2026 23:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 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-09T23:12:56.557287Z c9e49e7 Draft marked ready
ℹ️ 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: c9e49e7162

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

Comment on lines +231 to +233
if (reuseAcrossTurns && signal?.aborted) {
return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
}

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 Release the pooled session when abort wins the handoff

When a busy socket becomes idle, changed() reserves it before resolving acquireWaiting(). If the request is aborted after that resolution but before this async continuation runs, this branch rejects without releasing or disposing pooled. The entry consequently remains permanently busy, has no idle-expiry timer, and every later request for the same identity waits and then opens a throwaway socket until shutdown. Release/dispose the acquired session before rejecting, or transfer it to the exchange so the normal abort cleanup owns it.

Useful? React with 👍 / 👎.

Comment on lines +77 to +78
state.samples.push({ at: now, failed });
if (state.samples.length > MAX_SAMPLES) state.samples.splice(0, state.samples.length - MAX_SAMPLES);

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 Compute the ratio over the full 60-second window

Once an account records more than 512 terminal outcomes per minute, this truncation discards samples that are still inside the advertised 60-second window. For example, 500 failures followed by 512 successes in the same minute are treated as a 0% failure ratio rather than roughly 49%, allowing the account to recover incorrectly after the sustain period; reversing completion order produces the opposite result. Use bounded time buckets or aggregate counts so memory stays bounded without making high-concurrency health depend on the last 512 completions.

Useful? React with 👍 / 👎.

Comment on lines 461 to +462
const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
if (!upstreamResponse.ok) noteUpstreamRequestId(logCtx, headers);

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 Capture request IDs before streaming failures arrive

For a streaming response accepted with HTTP 200 that later emits response.failed, this condition is false at the only point where the upstream response headers are available. The SSE inspector subsequently records the terminal error code and status, but it cannot recover the header request ID, so the new diagnostics omit openai-request-id/x-request-id for this common failure mode. Store the bounded ID for successful streaming handshakes as well, while deferring the warning until a failure is actually observed.

Useful? React with 👍 / 👎.

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