Repository navigation
[Bug] Codex App (macOS): send button disabled at quota exhaustion - the #5947 intercept never sees the gate reads (they come from the bundled app-server) #6196
Description
Client or integration
Codex App (ChatGPT/Codex desktop for macOS)
Area
Platform (Windows / macOS / Linux) — where the desktop app's account-level send gate gets its reads
Summary
Two parts; part 2 is new evidence that may be useful for #5947.
1. Standing issue (same class as the open #4878 and the closed #5694 / #5797). Once the signed-in ChatGPT subscription quota is exhausted, the Codex desktop composer's send button is disabled and stays disabled no matter which model is selected — including models whose calls OpenCodex routes to independent third-party providers. v2.68.0 does not change this. Keeping the ChatGPT account signed in (so plugins, skills, marketplace and thread sync keep working) is a hard requirement in this setup; an authless / API-key-only layout is not an acceptable trade, because it removes the account from the UI.
2. New evidence — the #5947 approach cannot reach the gate reads on this desktop build. I replayed the 20-file change set from #5947 into the installed 2.68.0 package on this machine (PAC-fallback mode, chatgptDesktop = { unblockSend: true, pacFallback: true }), trusted the shared intercept CA, restarted the proxy so the loopback TLS listener came up on 10300 with the CONNECT entry on 10301, and let the app's own launch watcher relaunch ChatGPT with --proxy-pac-url=file://.../chatgpt-unblock.pac. The documented chain verifies end to end — curl -x http://127.0.0.1:10301 https://chatgpt.com/backend-api/wham/usage answers 401 through the listener with the certificate chain validating against the local CA. Then the account quota was actually exhausted, and the send button was still disabled.
The reason is that the intercept never sees the responses it rewrites. --proxy-pac-url (like --host-resolver-rules) is a Chromium switch, but on this build the chatgpt.com API traffic — including /backend-api/wham/usage[/stream] and the conversation-init reads that carry blocked_features / limits_progress — is issued by the app's bundled Rust codex app-server, not by the Chromium network stack:
- the desktop main process and its renderer hold no established TCP connections at all;
lsofshows the app-server holding thechatgpt.comconnections: one direct TLS connection out over the VPN's fake-IP interface, plus two to the local HTTP proxy on 127.0.0.1:7890;- over roughly 20 hours with the PAC in force, the listener sockets on 10300/10301 saw zero established connections;
- the desktop's own log agrees: the usage stream is delivered from the app-server side (
codex_usage_stream_terminated ... responseStatus=200 snapshots=59 reason=rotation), andchatgpt.comrequest failures surface throughAppServerConnectionwith Rustreqwest-shaped errors.
So rewrite.ts never gets a response to rewrite and the composer stays locked. That matches the PR's own note that it has not been checked against a running ChatGPT app.
I have since reverted the local port entirely (source, config, watcher, PAC), so this is a report about what the approach did here, not a request to keep an overlay alive.
Reproduction
- OpenCodex 2.68.0 on macOS, ChatGPT account signed in (
auth_mode=chatgpt), Codex desktop 26.924.22138 (build 11645). - Let the account's 5h / weekly ChatGPT quota run out.
- Open a Codex thread routed to a third-party model through OpenCodex.
- Observe: the send button is disabled and cannot be activated; switching models, including routed third-party models, does not re-enable it.
- For the feat(chatgpt-unblock): PAC-fallback mode so traffic survives opencodex stopping #5947 part: with the PR replayed locally and the app relaunched with the PAC switch, exhaust the quota again. Behaviour is unchanged, and the listener ports receive no traffic.
Version
2.68.0
Operating system
macOS 27.0 (arm64); ChatGPT/Codex desktop 26.924.22138, build 11645 (Codex framework 154.0.8037.57)
Provider and model
Not provider-specific. Reproduces with any routed third-party model, and with the OpenAI/ChatGPT lane itself.
Logs or error output
# the desktop app was launched with the PAC switch and ran for ~20 hours
$ ps -o command= -p <chatgpt-desktop-pid>
/Applications/ChatGPT.app/Contents/MacOS/ChatGPT --proxy-pac-url=file://<home>/Data/appdata/opencodex/chatgpt-unblock.pac
# the intercept listener never received a single connection
$ lsof -nP -iTCP:10300 -iTCP:10301
bun.exe 37717 <user> 11u IPv4 ... TCP 127.0.0.1:10300 (LISTEN)
bun.exe 37717 <user> 12u IPv4 ... TCP 127.0.0.1:10301 (LISTEN)
# ...while the bundled app-server holds the chatgpt.com connections
$ lsof -nP -a -p <app-server-pid> -iTCP -sTCP:ESTABLISHED
codex <pid> 11u IPv4 ... TCP 198.18.0.1:51544->202.160.130.52:443 (ESTABLISHED)
codex <pid> 17u IPv4 ... TCP 127.0.0.1:58550->127.0.0.1:7890 (ESTABLISHED)
codex <pid> 28u IPv4 ... TCP 127.0.0.1:61669->127.0.0.1:7890 (ESTABLISHED)
# desktop main process and renderer: no established TCP connections at all
$ lsof -nP -a -p <chatgpt-desktop-pid> -iTCP -sTCP:ESTABLISHED
(empty)
# desktop log, same conclusion
info [electron-message-handler] codex_usage_stream_terminated elapsedMs=1706321 error=undefined reason=rotation ... responseStatus=200 snapshots=59
error [electron-message-handler] Request failed ... error={"code":-32603,"message":"failed to list remote control clients: error sending request for url (https://chatgpt.com/backend-api/wham/remote/control/...)"} method=remoteControl/client/listScreenshots and supporting files
None.
Redacted configuration
{ "chatgptDesktop": { "unblockSend": true, "pacFallback": true } }(This block has since been removed again on this machine.)
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Questions
- Is [Bug] Codex Desktop model picker reopens after Luna Reserve exhaustion, but Send remains disabled #4878 the tracking issue for this, or would you like this kept separate? Happy to consolidate.
- Is there a supported path that keeps the ChatGPT account signed in? The only workaround I know of unblocks the composer by dropping the account (authless / API-key only), which loses plugins, skills, marketplace and thread sync.
- Does feat(chatgpt-unblock): PAC-fallback mode so traffic survives opencodex stopping #5947 (or feat(codex): add opt-in Windows desktop compatibility controls #6079) plan to cover the app-server side, or is interception expected to stay Chromium-only? If the gate is reachable through the Chromium stack on Windows, the same approach should not be assumed to work on macOS.
- The same problem class is reported upstream in openai/codex #47344, #48369, #48650 and #48816; one macOS workaround discussed there is rolling the desktop app back to 26.915.31945.
Activity
Ingwannu commentedon Sep 28, 2026
Confirmed as a P1 design blocker, not just a launch/readiness failure. The current #5947 implementation only influences Chromium with --host-resolver-rules / --proxy-pac-url and rewrites traffic that reaches its local listener. The supplied process/socket evidence shows this macOS build sources the authoritative WHAM/conversation gate through the bundled Rust app-server instead, so that listener cannot affect the send decision even when the PAC chain itself is healthy.
I am keeping this separate from #4878 because #4878 tracks the user-visible quota lock, while this report establishes why the proposed interception architecture cannot fix the current macOS client. A viable design must control or proxy the bundled app-server transport (or alter the gate through a supported upstream contract), preserve signed-in account features, and prove the exhausted-account -> independent-provider send flow on the actual app. Chromium-only tests are insufficient.
Ingwannu commentedon Sep 28, 2026
Draft design PR #6206 now records the supported boundary and recommended upstream provider-aware admission contract. It does not claim this issue is fixed: native hook ownership, implementation, and original-composer natural-exhaustion macOS UAT remain blocking. Independent design review is GO with no remaining P0-P2 in the record.
feat(chatgpt-unblock): route the bundled app-server's account reads t…
lcxhh521 commentedon Sep 29, 2026
Thanks for the detailed evidence, it was right: on this client build the gate reads come from the bundled app-server, not from Chromium. #5947 now also points the app-server at the relay through
chatgpt_base_url(details and measurements in the PR). One request and one limitation on the testing side:Could you confirm which quota state you were in? The report says "5h / weekly quota run out" and that the account quota "was actually exhausted", but not which window. These are different states in the usage payload (
primary_windowis the 5-hour window,secondary_windowthe weekly one), so please say which of these it was:What we can and cannot test. The test account behind #5947 is a Pro plan, which has no 5-hour limit, so we cannot reproduce "5-hour window exhausted, weekly quota still available". What we did verify is limited to the real bundled app-server reading a usage payload with a closed gate through the relay (
rate_limit.allowed: false,limit_reached: true,rate_limit_reached_type: rate_limit_reached), where it now reports the gate as open. We have not seen the composer itself re-enabled on a genuinely exhausted account, and we cannot exercise the 5-hour-only case at all. If you can, a run on the current head of #5947 in the state you had (with thechatgptDesktopblock set as in your report; the injectedchatgpt_base_urlthen appears in~/.codex/config.toml) would be the missing evidence, and please say which window was exhausted when you report back.feat(chatgpt-unblock): open the quota gate the app-server reports ove…
tone-leh commentedon Sep 29, 2026
Confirming the quota state from this machine, as requested — it is state (1): the 5-hour window exhausted while the weekly window still had room. The account here is exactly the plan class your Pro test account cannot reproduce.
Evidence (read-only, macOS, signed-in account):
account/readreturns{"type":"chatgpt","planType":"plus", …}— Plus, so a 5-hour window exists alongside the weekly one.shortPercent = 100,weeklyPercent = 32,shortWindowSeconds = 18000,resetCredits = 2(unused). In payload terms that israte_limit.primary_window.used_percent = 100withlimit_window_seconds = 18000(5 h), alongsiderate_limit.secondary_window.used_percent = 32(weekly). So: 5-hour 100 %, weekly 32 % — not both, and not the weekly one.lsof -nP -iTCP:10300 -iTCP:10301showed two LISTEN sockets and zero ESTABLISHED connections over 20 h, while the bundledcodex app-serverheld the chatgpt.com sockets.~/.codex/config.tomlhad nochatgpt_base_urloverride at any point.Local setup, in case it matters for the relay work:
26.924.22138(build 11645), Codex framework154.0.8037.57, macOS 27.0 arm642.71.0, third-party providers routed athttp://127.0.0.1:10100/v1; 8 local source patches, none of them touching the files feat(chatgpt-unblock): PAC-fallback mode so traffic survives opencodex stopping #5947 changesrequires_openai_auth = true,codexDesktopAuthless = false; no PAC, noCODEX_API_BASE_URL, nochatgpt_base_url. The left-bottom account avatar and the plugin / skill / marketplace / thread-sync features have to stay intact, so authless / API-key-only shapes are not acceptable as a fix here.Two questions about the new
chatgpt_base_urldirection, if you can answer them:/backend-api/wham/usage(plus its SSE), or also/backend-api/conversation/init(theblocked_features/limits_progresspart)?chatgpt_base_urlonly read at app-server start (so Codex must be fully restarted), or can it take effect while running?I can capture relay-side numbers the next time this account's 5-hour window naturally exhausts; I don't burn the window deliberately, so tell me if you want anything specific recorded when that happens.
feat(chatgpt-unblock): route the bundled app-server's account reads t…
feat(chatgpt-unblock): open the quota gate the app-server reports ove…
test(chatgpt-unblock): cover the 5-hour-only exhaustion reported in l…
8 remaining items
Ingwannu commentedon Oct 1, 2026
Thank you for the single-variable Group A evidence. The recorded bundled-or-dev launch with zero shim-script occurrences, usable Send, and established Chromium connections to the intercept support the reported result for your locally ported 2.73.0/build 26.928.21956 topology. This supersedes the earlier zero-established-connections observation for that topology; we should not keep presenting the shim as the measured active mechanism.\n\nPlease clarify one field from the existing saved observation only: does the reported five-hour 0% mean remaining capacity (exhausted), rather than utilization? The previous report used 100% for exhaustion. No new prompt or deliberate quota consumption is requested.\n\nLimits remain important: this is no-shim Group A evidence, not shim-only sufficiency; the inverse control was not run, and no actual routed model request was submitted in this A/B. All-null RPC payloads explain why no rewrite was observed, but do not prove the app-server can never receive quota data on other builds/topologies. I will flag this correction on the split intercept PR #6365; the local-CA/account trust decision and missing non-quota/live client acceptance remain separate. Please share only minimal redacted excerpts if needed, not the full raw artifact directory.
lidge-jun commentedon Oct 1, 2026
Resolved on
devby #6361 (squashfcbfb16c00), an opt-in experimental shim for macOS. SetchatgptDesktop.appServerShim: true, then runocx chatgpt launch. ChatGPT relaunches with a launcher that filters the bundled app-server's stdout. Inaccount/rateLimits/readandaccount/rateLimits/updated, the shim clears only the plain-quota gate (rate_limit_reached, a window at 100%), so Send stays enabled for routed models while displayed usage stays accurate. Workspace, credit and spend-control restrictions stay closed. This follows the approach @TooSpace verified on a genuinely exhausted Plus account on 10/01. Thanks to @TooSpace and @Ingwannu for the evidence and the design analysis.ocx chatgpt restoreundoes it. If Send still stays disabled with the shim active, please reopen and include therateLimitReachedTypethe app-server reported.feat(codex): window-aware main-account hard-lock thresholds and outsi…
feat(chatgpt): add experimental macOS app-server quota-gate shim
feat(chatgpt): carry the local-CA send-unblock intercept (split from l…
tone-leh commentedon Oct 2, 2026
Follow-up from the reporter of the original report, two days on and after 2.76.
Thanks again for #6361 and for splitting #5947. Reporting back as you asked, since the situation on this machine is now clear enough to be useful.
What we run now
2.76.0, macOS 27.0.1 arm64, signed-in Plus account. No local port, no certificate, no listener, no watcher: the feat(chatgpt-unblock): PAC-fallback mode so traffic survives opencodex stopping #5947 port we had been carrying was retired once feat(chatgpt): experimental macOS app-server quota-gate shim (split from #5947) #6361 landed.chatgptDesktop = {"appServerShim": true}, armed through the documented flow.What the shipped half covers, and what it does not
ocx chatgpt statusreportsCODEX_CLI_PATH launcher: yeson a launched app.ocx chatgpt launchagain. On this machine the app is reopened several times a day, so in practice "keep it applied" means "never quit the app".The half we could not observe working, which is why this matters to the review
appServerShim: falsewith the intercept enabled, and Send stayed usable. In the same session the filter loggedaccount/rateLimits/*payloads whose fields were allnullexceptlimitId, so it had nothing to clear.One thing we hit that may be worth a small fix
unblockSend) sat next toappServerShiminsidechatgptDesktop. The strict schema rejects the whole block, and the read path degrades it to "absent", so the shim read asoff,ocx chatgpt launchrefused, and nothing armed it.{"appServerShim": true}, a naive check of that key passed, andocx chatgpt statusgave no reason.chatgptDesktopConfigErrorwould name the rule, but it only guards live writes.ocx chatgpt status(and ideally in the proxy config diagnostics) rather than degrading it silently. I am happy to file this as its own report if you would rather track it separately.Workaround we are on meanwhile
launchctl setenv CODEX_CLI_PATH <launcher>, re-set at login by a LaunchAgent. Dock launches inherit the launchd user domain, so the shim attaches with no relaunch and no per-launch command. Trade: while the variable is set,ocx chatgpt restoreno longer takes effect untillaunchctl unsetenv.Offer
Thanks for the work here. The split was the right call, and the guide is unusually candid about what the shim does not do.
tone-leh commentedon Oct 2, 2026
Reverse test at natural exhaustion: shim active, 5-hour window at 100%, Send still disabled
Your closing note said to come back with the
rateLimitReachedTypeif Send stayed disabled with the shim active. The condition is met, so here is the observation — and first, why we think it is trustworthy.What was actually active (verified at the moment of the observation)
2.76.0, upstream-only:chatgptDesktop = {"appServerShim": true}, armed throughCODEX_CLI_PATHin the launchd user domain (see the previous comment). No local port, no certificate, no listener, no launch switches, no watcher.CODEX_CLI_PATH=<launcher>.ocx chatgpt statusreportedapp-server shim: onandCODEX_CLI_PATH launcher: yes. Five processes matchedchatgpt-app-server-filter, and the launcher's--self-testexited 0. So the filter was attached and passing its own precondition, not merely installed.{"rateLimits":{"limitId":"codex","primary":{"usedPercent":100},"rateLimitReachedType":"rate_limit_reached"}}) and it rewroterateLimitReachedTypetonull. The rewrite logic itself works.The observation
/api/provider-quotas(source: chatgpt:wham, generated within the same minute as the report).Why this is the other half of the earlier A/B
Read together, these point at the Chromium-side intercept as the mechanism that actually clears this gate on macOS, and at the merged app-server shim as not sufficient on its own.
What we cannot tell you, and what would settle it
rateLimitReachedTypeyou asked for: the shipped filter does not log, and we have no supported way to observe the app-server'saccount/rateLimits/read/account/rateLimits/updatedpayloads. So we cannot separate two readings of the result: either the shim did clear a plain-quota flag and the app gates on something else, or the gate never arrives through that pipe at all — which is what our 2026-10-01 filter log showed, where every field exceptlimitIdwasnull.Reopen? The condition in your closing note is met, but we are missing the field you asked for, so we are commenting rather than reopening. Say the word and we will reopen it with this as the body.
cc @lcxhh521 @lidge-jun — this is the reverse of the 10-01 A/B and is directly about the review decision in #6365 and #6381.
One thing that is independent of how the mechanism question lands: without the launchd-env recipe, every cold start needs a manual
ocx chatgpt launch, so #6381 looks valuable on its own terms.lcxhh521 commentedon Oct 3, 2026
Thanks @tone-leh, the reverse test is the most useful data point in this thread.
What the two runs show together. On 10-01, with the intercept on and the shim off, Send stayed usable at exhaustion. On 10-03, with the shim on and no intercept, Send stayed disabled. On current macOS builds, the Chromium-side intercept (#6365) is the part that carries this gate. @lidge-jun, the "evidence against" section in #6365 cites the 09-27..29 zero-connection reading; these two runs are the newer data.
A correction about #6381. Its watcher only re-applies the intercept's launch switches:
install-watcherrequireschatgptDesktop.unblockSend, and the agent it installs does not add the shim. With the shim alone it does not help you stay attached. Once #6365 lands and the intercept is on, the watcher re-routes Dock/Spotlight launches, and with #6381 it also re-routes an app that was already open when opencodex starts.The silent config drop. Confirmed. #6486 names the failing field in
ocx chatgpt status, in the config diagnostics and as a load-time warning, andocx chatgpt launchgives it as the reason it refuses.The
launchctl setenvworkaround. It works because the launchd user environment reaches apps opened from the Dock. One trap:ocx chatgpt restoredeletes the launcher, but the variable keeps pointing at it, so the app would then try to start its app-server from a file that no longer exists. Runlaunchctl unsetenv CODEX_CLI_PATHbeforerestore, or skiprestorewhile the variable is set.Capturing the RPC payloads. That is a change to the shim's filter, so I'll leave whether to add an opt-in capture to @lidge-jun.
fix(chatgpt): say why a dropped chatgptDesktop block reads as off (#6486
Update from upstream 2.80.0 while preserving fork integrations (#9)
tone-leh commentedon Oct 10, 2026
Thanks @lcxhh521, that matches what I saw. Good catch on the
launchctl unsetenv CODEX_CLI_PATHstep beforerestore, I'll do that. I'll test #6365 with the watcher once it lands, and report back with the quota window and flags at my next natural exhaustion. Happy to try an opt-in RPC capture if @lidge-jun adds one.Fakaren commentedon Oct 13, 2026
Adding independent forensics from a 26.1007.30912 (macOS arm64, OpenCodex 2.84.0) machine that
confirms the post-closure A/B in this thread — the appServerShim is correct but its channel
carries no gate data on this build:
zero
account/rateLimits/*messages (noordinaryUsageAllowedsubstring anywhere), yet thequota banner renders and the local-host composer send button stays disabled — the UI's gate
input never crosses the app-server pipe.
submitDisabledhas 8 terms; only
un = q(FN) && host === 'local'is true. TheFNderived atom reads asnake_case rateLimitStatus (
rate_limit.allowed,rate_limit_upsell, …) delivered to therenderer over the SSE/polling usage transport (
/backend-api/wham/usage[/stream]per thisissue; page-level fetch/XHR hooks see nothing either — native bridge), and its only
"acknowledged" write path fires on reset-credit redemption (
outcome: 'redeemed'), so anexhausted 5h window hard-locks the local composer until reset or credit burn.
account/rateLimits/readpayloadoffline it correctly clears
rateLimitReachedType/ordinaryUsageAllowedwhile preservingusedPercentand spend-control. The v2.84.0 test suite covers only the camelCaserateLimits/rateLimitsByLimitId/ordinaryUsageAllowedshapes — the renderer snapshot(snake_case) and the wham/usage SSE stream are structurally out of its reach, so no OpenCodex
version bump fixes this on such builds.
(single equal-length expression change, no on-disk bundle modification, fail-open), the composer
submits; cloud models still hit genuine 429s through the proxy, and a local
dave-linux/qwen3.8-flash-next-iq3_sturn completes200in ~42s. Banner and usage percentsstay truthful. Not attaching that implementation here since it intercepts closed-source Desktop
JS — happy to share details with maintainers if useful (it also matches the Chromium-side
intercept A/B result from 10-01 above).
Suggest reopening or tracking under #6206-style design work: any fix must target the renderer
transport (wham/usage SSE / conversation-init) or upstream in openai/codex (#47344/#48369/#48650)
to scope the gate per provider.
tone-leh commentedon Oct 13, 2026
Could you reopen this issue, or point to a concrete follow-up issue tracking the remaining macOS send lock?
account/rateLimits/*messages in the captured app-server stdout while the composer stayed locked. Their findings suggest the renderer's quota transport bypasses the shim; this is their observation, not a new reproduction on my machine.The post-closure evidence shows that an attached shim alone was insufficient on these builds. The unresolved requirement is still to allow independently funded routed models while keeping the ChatGPT account and its features signed in.