Repository navigation
Description
Client or integration
Codex App (macOS), using OpenCodex's canonical OpenAI / Codex-login route.
Area
Proxy / routing — upstream HTTP/SSE transport, connection-reset handling, synthetic 429 diagnostics.
Summary
Intermittent GPT turns fail in Codex App with exceeded retry limit, last status: 429 Too Many Requests. OpenCodex's own usage log identifies these not as an upstream quota rate_limit_exceeded, but as locally generated HTTP 429 responses with errorCode=upstream_reset_replay_refused and:
The upstream exchange did not complete reliably. The request may already have been processed; automatic replay was stopped.
The workflow was stable for months before recurrent failures in October 2026. The local custom model-router on port 10101 has been stopped and removed from Codex global routing; the active entrypoint is the OpenCodex proxy on 127.0.0.1:10100. The failures persisted after upgrading OpenCodex to v2.91.0. An operator check on Oct 25 confirmed providers.openai.upstreamWebsocket=false (HTTP/SSE selected); the exact transport state at each prior log timestamp was not independently recorded.
This report requests better transport attribution and an actionable, safe mitigation. It does not request blindly replaying an ambiguous request that may already have been processed.
Reproduction
Intermittent; not yet a deterministic minimal reproduction.
- Start Codex App with OpenCodex v2.91.0 routing official GPT models via the canonical
openai(Codex login) provider. - Use GPT-6 Sol or GPT-6.1 Sol in ordinary long-running Codex project turns.
- Some requests stop and Codex displays
exceeded retry limit, last status: 429 Too Many Requests. - Correlate the time with
~/.opencodex/usage.jsonl.status=429,errorCode=upstream_reset_replay_refused, and the aboveupstreamErrorare logged.
Version
- OpenCodex: v2.91.0 (latest explicitly verified locally at time of report).
- Codex desktop app bundle: 26.1016.62342.
- Separately installed terminal Codex CLI: 0.144.1 (not asserted to be the App's bundled runtime).
- Prior duplicate npm CLI 0.142.4 removed; it did not resolve the issue.
Operating system
macOS. Exact OS build/architecture not collected in the sanitized diagnostics.
Provider and model
Canonical openai / Codex login, gpt-6-sol and gpt-6.1-sol. No API credential information shared.
Logs or error output
Redacted examples from the local usage log, times in UTC+08:00:
| Local time | Provider/model | status | errorCode | durationMs |
|---|---|---|---|---|
| 2026-10-23 18:59:36 | openai / gpt-6-sol | 429 | upstream_reset_replay_refused | 1103 |
| 2026-10-23 18:59:46 | openai / gpt-6-sol | 429 | upstream_reset_replay_refused | 1034 |
| 2026-10-23 19:00:00 | openai / gpt-6-sol | 429 | upstream_reset_replay_refused | 885 |
| 2026-10-23 20:23:43 | openai / gpt-6.1-sol | 429 | upstream_reset_replay_refused | 5131 |
| 2026-10-23 21:48:39 | openai / gpt-6.1-sol | 429 | upstream_reset_replay_refused | 5215 |
| 2026-10-24 11:31:54 | openai / gpt-6-sol | 429 | upstream_reset_replay_refused | 5228 |
Distinct issue: two MiniMax-M2.7 429s in this log were genuinely returned by the MiniMax provider, with an explicit Token Plan usage-limit message. Those should not be conflated with the synthetic OpenAI-route 429s above.
Redacted configuration
- Codex's root
openai_base_urlpointed tohttp://127.0.0.1:10100/v1during the current OpenCodex-integrated setup. - Previously installed custom local proxy
127.0.0.1:10101is no longer listening or configured in the root Codex path. providers.openai.upstreamWebsocket=falseverified on Oct 25; no claim that this was recorded at each earlier event.- No auth tokens, emails, cookies, account IDs, request bodies, local project names/paths, or full logs included.
Expected behavior
Stable canonical OpenAI forwarding, or precise stage-level diagnostics and a safe mitigation if an ambiguous upstream reset occurs. A proxy-local safety refusal should be distinguishable from upstream rate limiting in user-facing errors and account health tracking.
Questions for maintainers
- On v2.91.0 with HTTP/SSE, which exact upstream transport stages can produce
upstream_reset_replay_refusedfor canonical OpenAI forwarding? Does the code distinguish an upstream idle-socket reuse/reset from TLS/proxy failures or an upstream response? - Are there verified connection-pool or pre-send transport mitigations that preserve the OpenCodex graphical multi-provider model picker, without unsafe duplicate sends?
- Is there a version/PR after v2.91.0 that actually fixes the underlying connection reset, as opposed to changing the classification or retry decision?
- Which sanitized fields should be captured for the next occurrence (
attempts.failureStage,failureCause,transportPhase,sendCount,resendPermission, upstream headers,requestId) to confirm root cause?
Checks
- Searched existing issues (including exceeded retry limit, last status: 429 Too Many Requests after my ChatGPT quota reached its limit. #6658, [Bug]: translated Responses→chat dispatch never wires claimAmbiguousResend — providers.<name>.retryOnReset cannot take effect and a pre-header reset always answers 429 #6510, [Enhancement]: Mid-stream OpenAI socket resets can break Codex account affinity #919 and [Bug]: ChatGPT upstream WS closes 1008 after 1,589 output events on 2.84.0 #7073).
- Removed secrets, credentials, request bodies, private paths and account identifiers.
- Did not claim that the upstream OpenAI service itself sent the synthetic 429.
Activity
Ingwannu commentedon Oct 25, 2026
I checked the released
v2.91.0tree (83ec4f75a4) and currentdev(6672cfb4a2). This is a local replay-safety refusal, not evidence that OpenAI rate-limited the account. The distinction in this report is correct.isConnectionResetErrorandfetchWithResetRetryrecognizeECONNRESET,EPIPE, and two reset/closed-socket message shapes before the fetch has returned a response. They deliberately do not classify ordinary aborts/timeouts, DNS, connection refusal, or an ordinary HTTP error response as that first reset. The code does not prove whether a recognized reset came from idle connection reuse, an intermediary, or the origin, nor whether the POST executed. If an operator replacement was already spent, other replacement failures/statuses can also settle as the same refusal. Consequently the code alone is not a unique stage/root-cause identifier.applyUpstreamRecoveryInitdisables keep-alive on a permitted replacement send. That is not a verified pre-send remedy for your incident, and enablingretryOnResetauthorizes potentially duplicate inference. I do not recommend changing that setting or selecting a different transport as a proven fix on this evidence. The existing provider picker can remain in use while we collect the next failure.status,errorCode,transportPhase, and the attempt'sfailureStage,failureCause,sendCount,recoveryKinds,recoveryWithheld, and content-freecodexWsStageonly when those fields exist.resendPermissionis derived from the stage/cause, not a stored usage field; HTTP rows need not contain WS telemetry. Keep absent values absent. A locally consistent replacement for the request ID is sufficient for correlation; omit account labels, credentials, request content, full headers and raw exception text.Leaving this open for transport attribution. The existing refusal deliberately discards the raw reset exception, so the six supplied rows cannot distinguish socket reuse from another reset source. A narrow diagnostic improvement should retain only bounded transport facts and must preserve the no-duplicate-send boundary.
runshen0 commentedon Oct 25, 2026
Follow-up with four new, redacted usage-log records from Oct 25, 2026, as requested. These are from the same macOS Codex App / canonical
openaiprovider setup as the original report. All timestamps are UTC+08:00. OpenCodex v2.91.0 was last verified in the environment;providers.openai.upstreamWebsocket=falsewas read back on Oct 25 at approximately 12:02 UTC+08, but there is no contemporaneous per-request transport snapshot, so I cannot assert it was the transport mode at each failure. The exact Bun and macOS build versions were not captured. No account IDs, session content, headers, raw exceptions, or raw request IDs are being shared.gpt-6-lunaupstream_reset_replay_refused6f022c706121gpt-6-lunaupstream_reset_replay_refused6ba7821d5b40gpt-6-lunaupstream_reset_replay_refusede0c633187191gpt-6-lunaupstream_reset_replay_refusedbfcff13fd6bdEach record's top-level diagnostic fields are identical:
{"failureStage":"headers-only","failureCause":"rate-limit","status":429,"errorCode":"upstream_reset_replay_refused"}Each has exactly one reported attempt with the same stage/cause/error, and:
{"sendCount":1,"recoveryKinds":[]}The inspected output did not include
transportPhase,recoveryWithheld, or a top-levelcodexWsStageobject. They are absent from the supplied extraction; please don't infer values for them. Request correlation strings above are truncated SHA-256 hashes of local request IDs, not the original request IDs.Specific question prompted by these records: how can
failureStage=headers-onlyandfailureCause=rate-limitcoexist with the syntheticupstream_reset_replay_refusedcode on a one-send attempt (sendCount=1, norecoveryKinds)? Does this indicate (a) a real upstream HTTP 429 being projected into the proxy's no-replay safety code, (b) an ambiguous transport reset after a headers-only upstream exchange, or (c) a classification/projection bug? The 136.8-second record is particularly different from the other 0.6–5.4-second failures.As your previous triage notes, the synthetic public 429 is not proof that OpenAI rate-limited the account, but
failureCause=rate-limitalso should not be silently discarded. Please advise a safe way to capture the actual upstream status / pre-header stage (without credentials or content and without ambiguous duplicate sends) and whether the available fields can distinguish these cases. The aim remains to keep the Codex App graphical third-party model picker operational.No
retryOnResetchange or live fault injection was made to produce these records.Ingwannu commentedon Oct 25, 2026
The new rows expose a real attribution question;
headers-onlyplusrate-limitshould not be treated as proof of an upstream quota response. I found and reproduced one concrete projection defect on currentdevand opened #7727 (Draft,1119578e189063963234d6db16dcf6ba6299d624).The two transports are independent:
upstreamWebsocket=falsechooses HTTP upstream, but Codex App can still enter OCX through client WebSocket ingress. The HTTP ingress already stamps the in-process replay-refusal marker asterminalSource: synthetic/failureCause: transport-ambiguous; the WebSocket ingress finalizes its own log and omitted that stamp. Its numeric429 fallback can therefore report rate limiting despite an ambiguous reset.headers-onlyis the recorder's response-progress stage; it does not establish that OpenAI returned headers or executed the request.In the new hermetic fixture, canonical Codex-login and keyed-gateway HTTP fetches each throw a synthetic ECONNRESET. Both send exactly once, have no recovery and expose the replay-refusal429 to the WebSocket client. Before the patch both fail the provenance assertion (the stored code also becomes
rate_limit_exceededin this current-dev path); afterwards the row/attempt retain the refusal code andtransport-ambiguous. A real upstream429 remainsrate-limit. The regression plus existing attribution controls pass 19 tests / 22652 assertions. The stored-code difference from your extraction is another reason not to claim every historical row is now explained.This fixes diagnostic provenance only, not the underlying reset. Your missing transport fields remain unknown, and neither 136.8s nor 0.6–5.4s identifies the connection phase by itself. No duplicate send or change to
retryOnResetis proposed. Until a build containing the patch is available, preserve the closed fields already supplied, the exact OCX/Bun/OS versions and whether the request used client HTTP versus WebSocket when that is actually observable; do not share credentials, headers, account IDs or content. A missing field should stay recorded as absent.The PR remains Draft: focused/layout/structure/privacy checks pass, while local full TypeScript and docs search-index generation hit the existing five-minute resource envelope; exact-head hosted CI is pending. The issue remains open for the original transport-reset investigation. No live fault injection, account operation, config edit or Codex Security scan was performed.
runshen0 commentedon Oct 26, 2026
Additional operator observation on 2026-10-26 (UTC+08:00): the user changed the VPN egress from a Singapore endpoint to a Los Angeles (US) endpoint and still sees the same Codex UI message
exceeded retry limit, last status: 429 Too Many Requestsduring ordinary project work. This change did not provide reliable relief.Attribution limits: No matching post-switch
usage.jsonlrow has been supplied yet, and the actual OCX process-level egress/peer routing on both endpoints was not captured. We therefore cannot assert that the new user-visible 429 is specifically anotherupstream_reset_replay_refused, or that different public IPs were definitely used by OCX; the observation only weakens the simple "Singapore VPN geography alone" hypothesis. No raw IP or network subscription details are shared.I also saw the maintainer's draft #7727 and understand it fixes WebSocket ingress log attribution, not the underlying reset. Thank you for the precise distinction.
Focused maintainer question: is
OCX_FRESH_CONNECTION_HOSTSapplicable and safe on the canonical ChatGPT-login OpenAI HTTP/SSE forwarding path in v2.91.0, and has anyone established it reduces pre-header ambiguous resets there? The reporter wants to preserve the GUI model picker and should not enable ambiguous POST retransmission. If not verified, please say so rather than recommend a speculative production tweak. We can provide a next failure's content-free transport fields once captured.jingyihu818 commentedon Oct 26, 2026
Hi, I'm seeing a closely matching failure on Windows using OpenCodex with the official OpenAI (Codex login) provider.
A fresh occurrence from the OpenCodex UI:
gpt-6.1-sol, high reasoning effort429errorCode: upstream_reset_replay_refusedIn several runs, I subjectively noticed the failure appearing about 15 seconds after response activity begins. This is a UI observation, not yet a precisely measured transport interval. I can collect redacted logs for a future occurrence.
I understand from the maintainer discussion that this is a proxy-generated replay-safety refusal, not proof of a genuine upstream OpenAI rate limit, and that draft PR #7727 fixes log attribution rather than the underlying reset.
A few questions:
retryOnReset?Thanks for investigating. I can follow up with content-free logs and version details.
fix(logs): preserve replay refusal attribution on WebSocket ingress (#…
lidge-jun commentedon Oct 27, 2026
Partial progress merged into
dev: #7830 (merge commit 7deb359, carrying #7727).On a direct provider route, a proxy-generated
upstream_reset_replay_refused429 now keeps itstransport-ambiguouscause and a synthetic terminal source on WebSocket ingress too, matching HTTP ingress. Usage logs and account health no longer show it as a provider rate limit. A genuine upstream 429 keeps its rate-limit attribution.Still open:
src/server/responses/core-combo-failure.ts,consumeComboFailure), the replay-refusal marker is not carried over, andadoptFailedChildLogkeeps the child attempt as recorded. A refusal served through a combo is therefore still attributed as a rate limit on both HTTP and WebSocket ingress. Fixing it needs the marker carried through the rebuild, the child attempt stamped, and regressions on both ingresses.Leaving this issue open.
fix(responses): preserve combo replay-refusal attribution (#7864)
lidge-jun commentedon Oct 28, 2026
Combo follow-up merged into dev: #7864 (f26d6e8). A proxy-generated upstream_reset_replay_refused 429 served through a combo now keeps the refusal code, the transport-ambiguous cause and a synthetic terminal source on both HTTP and WebSocket ingress, on the logical row and on the serving child attempt. It no longer produces retry-after or quota reset metadata for combo cooling. A genuine upstream 429 through a combo keeps rate-limit attribution.
Still open: the underlying connection resets and a transport mitigation. Both #7830 and #7864 fix attribution only, so the next occurrence is reported accurately.
leonforgit commentedon Oct 28, 2026
Additional evidence: synthetic 429 across two unrelated upstreams in the same two-minute window
Environment
127.0.0.1:10100,websockets=falseinconfig.json(no per-request transport snapshot was captured), default provideropenai(authMode=forward,codexAccountMode=pool,baseUrl=https://chatgpt.com/backend-api/codex).deepseek(baseUrl=https://api.deepseek.com,responsesPath=/responses,statelessResponses=true). The failing attempt itself was recorded withadapter=openai-responsesandprotocolTrace.mode=native/reasonCodes=[same-wire-native], so both routes were native Responses rather than a translated Chat path.pmset -g logshows no system sleep in the window; only display assertions.What happened (UTC+08:00)
Four synthetic 429s in 111 seconds, split across two different upstreams:
All four rows report
errorCode=upstream_reset_replay_refused,sendCount=1,recoveryKinds=[],resendPermission=refused-ambiguous,terminalSource=synthetic.The point I want to add to the thread: the first two requests go to
chatgpt.comand the last two go toapi.deepseek.com. They share only the local egress path. A capacity 429 at one provider cannot explain a pre-header failure at the other 100 seconds later.Normal requests resumed at 11:37:53 on both providers with no configuration change, so this is a burst, not a steady state.
This is not the quota class
The same log does contain genuine rate-limit 429s, and they look different:
openai / gpt-6.1-terraateffort=max,failureCause=rate-limit, durations 0.7-11.3 s.gpt-5.6-luna(61 requests) andcodex-auto-review(61 requests) had zero 429s.So I am not claiming the rate-limit rows are synthetic. The two classes appear distinguishable by
failureCause, which is useful.Diagnostic gap
The synthetic rows carry no
transportPhasefield; successful rows in the same log do (terminal_sse). Without it the log cannot distinguish:Only (1) is clearly safe to replay without the duplicate-inference risk that makes
retryOnResetunacceptable as a default.Request
Happy to provide more rows or run a specific probe if that helps.
Ingwannu commentedon Oct 29, 2026
Following up on the unanswered connection-policy and phase questions, against trusted dev
9b78a16f4fe0c300527e6b183052407269bb941dand the actualv2.91.0source:OCX_FRESH_CONNECTION_HOSTSdoes apply to a matching HTTP physical send, including the canonical ChatGPT HTTP/SSE path, in both revisions.src/server/responses/fetch-helpers.tsmatches the final rewritten destination hostname (exact host or subdomain), and passesConnection: closepluskeepalive: falseto the selected fetch. It does not add a replay or choose a different model. It is an HTTP policy, not a WebSocket policy. This establishes implementation applicability, not a verified reduction of production resets: there is no controlled before/after capture here, and I cannot present it as a proven mitigation. No live configuration was changed or provider call made.stage: headers-only,sendCount: 1, or rawECONNRESETdoes not prove that no request bytes reached the upstream. MissingtransportPhaseis unknown, notnever-sent. The synthetic 429/reset-refused class must remain distinct from a genuine upstream quota 429. Failures across two hosts do not by themselves locate the cause at the client, relay or provider.websockets=falsesetting alone is not an observed per-request upstream transport record; upstream WebSocket eligibility has its own provider/request selection. Please retain the actual selected transport when available rather than inferring it from that flag.For a further redacted sample, the useful bounded fields are build/version, selected HTTP/WS transport, public destination hostname, elapsed duration, terminal code/class/source, send count, any explicit transport phase and recovery trail; absent fields should stay absent. Please omit headers, credentials, account identifiers and request content. Keeping this issue open for the transport root cause and a reproducible phase distinction.
github-actions commentedon Nov 2, 2026
Automated translation bookkeeping — detected language: English.
lidge-jun commentedon Nov 10, 2026
Follow-up: #8486 merged into
devas c225873. It does not resolve the underlying reset, so this issue stays open. It changes two things relevant here:[upstream-retry] ambiguous reset not replaced (<host>, <stage>): ambiguous-no-policy | ambiguous-request-not-replayable | ambiguous-allowance-spent. The line carries only the host and stage, never request content or credentials. On a canonicalopenairow withoutretryOnReset, expectambiguous-no-policy, which means the refusal is the fail-closed default, not a judgment about the turn.retryOnResetnow get the replacement on chained Codex turns too. Previously aprevious_response_idthe proxy had already expanded locally made every chained turnambiguous-request-not-replayable([Bug]: retryOnReset replacements never spent for key-auth openai-responses providers — every pre-header reset answers synthetic 429 (v2.99.0) #8453).For a future occurrence, the most useful data is that log line next to the matching
usage.jsonlrow (status,errorCode,failureStage,sendCount, duration). That pins down which stage reset and whether the opt-in applied. The transport root cause, including the recurring ~15.7 s per send reported in #8453 and here, is still unexplained.lidge-jun commentedon Nov 15, 2026
On the next occurrence, please post the
[upstream-retry] ambiguous reset not replaced (...)log line (added by #8486) together with the matchingusage.jsonlrow (status,errorCode,failureStage,sendCount, duration).