[Bug]: Main-account quota lock stalls Codex Goals past another pool account's recovery
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- bun, typescript
- Domain
- authentication, backend
Research direction
Start with src/codex/auth-context.ts, especially cooldownErrorResponse() and the CodexMainAccountHardLockError path; the issue says this function is unchanged in v2.84.0. Compare the main-account response contract with the documented isolated Codex app-server reproduction and existing quota error handling. Done means the Goal reports the refusal accurately and pool-backed Goals can re-evaluate eligible accounts after recovery, while preserving pins, replay safety, credits consent, and the quota guard; the issue notes that the narrow error-contract mitigation alone does not complete this work.
Written by the indexing model from the issue text.
Description
Client or integration
Codex App
Area
Authentication and account pool
Summary
The local OpenAI main-account quota guard returns 429 rate_limit_error / rate_limit_exceeded with a weekly Retry-After. Codex treats this as a transient failure and sleeps until that deadline while the Goal remains active.
In a real incident, Retry-After: 446329 scheduled the first retry 5 days, 3 hours, 58 minutes, 49 seconds later. The task made no further model requests for 4 hours, 16 minutes, 37 seconds, until I interrupted it. The app continued to show the goal as running.
The quota guard itself should remain in force. Further production evidence shows that pool-to-main handoff did succeed, but the client then slept past another pool account's recovery. There are two distinct concerns: reporting the local refusal accurately, and recovering an unpinned pool-backed Goal when another eligible account becomes available. Returning usage_limit_reached fixes the misleading active/error state in the isolated test below; it does not implement automatic continuation and is not a complete fix for this pool workflow.
This is the OpenAI main-account path from #6000, which was closed by the template bot rather than resolved. #6991 / #6995 fixed the related Anthropic path, but cooldownErrorResponse() in v2.84.0 still has the same implementation as v2.83.0.
Additional verified pool timeline (2026-10-13, UTC+08:00)
| Time | Observed evidence |
|---|---|
| 10:20 | The preferred pooled account reported five-hour usage 100% and weekly usage 80%; its five-hour reset was advertised for 14:03:19. The same conversation moved to the caller-owned main-account path. |
| 10:20–12:35 | 159 successful responses and one transient 502 followed on that path. Client quota events showed a single weekly window increasing from 28% to 98%. This was a successful handoff, not a failure to select main. |
| 12:32:56 / 12:35:02 | Actual HTTP response headers and a client quota event reported primary-used-percent=98, primary-window-minutes=10080. The next request hit the main-account guard. |
| 14:04:05 | A fresh response-header quota-history observation for the pooled account, under the same credential generation, reported five-hour usage 23% and weekly usage 4%. The affected turn made no new request and therefore did not reselect it. The reason for the weekly decrease was not established. |
| 16:51 | After manual interruption, a new turn successfully used the pooled account again. |
This combines conversation-scoped request records, client quota events, actual response headers, and timestamped per-account quota history; the account attribution is not based solely on a missing pool label. The 14:04 observation demonstrates restored quota headroom, not a successful request by the affected turn or a guarantee that every routing eligibility check passed at that instant.
The expected pool behavior is to keep using main while it is eligible, then expose a truthful unavailable/waiting state and re-evaluate the pool when capacity recovers. A wait tied only to main's weekly reset overlooks a sibling that can recover much earlier. Merely ending the Goal as usageLimited is an error-contract mitigation, not this end-to-end recovery behavior. Any recovery design must preserve account/model eligibility, explicit pins, replay safety, credits consent, and the main-account guard; a reset timestamp alone is not proof of fresh quota.
Reproduction
Observed production path
- Use a native OpenAI model through OpenCodex with the main-account quota protection enabled and a Goal active in Codex.
- Let a main-account request hit that protection. In this incident the refusal explicitly named the default policy: five-hour usage ≥90% or weekly usage ≥98%.
- OpenCodex refuses locally with a retryable 429 and
Retry-Afterpointing to the weekly reset. No upstream model attempt is recorded for this refusal. - Codex schedules a retry for that deadline and leaves the Goal active, instead of surfacing a terminal usage-limit state.
Isolated reproduction; no quota exhaustion or real provider required
The response can be constructed directly from OpenCodex's code, with independent temporary OPENCODEX_HOME and CODEX_HOME directories set before starting Bun:
import { CodexMainAccountHardLockError, cooldownErrorResponse }
from "./src/codex/auth-context";
const now = Date.now();
const response = cooldownErrorResponse(
new CodexMainAccountHardLockError(now + 446329000), now,
);
console.log(response.status, response.headers.get("retry-after"));
console.log(await response.json());
// 429, 446329; error.type=rate_limit_error,
// error.code=rate_limit_exceeded
I served that exact generated status/body/headers from a local mock /v1/responses endpoint to the actual bundled Codex app-server, using a separate empty CODEX_HOME. The model-provider fixture used wire_api="responses", requires_openai_auth=false, and request_max_retries=0; the normal stream retry policy remained enabled.
The app-server protocol sequence was:
initialize, withcapabilities.experimentalApi=true; theninitialized.thread/start, with modelgpt-6-astra, the loopback fixture provider and an isolated working directory.thread/goal/set, withobjective="Isolated quota contract test",status="active",origin="user".turn/start, with one text input,Reply OK.- Read error/turn notifications and
thread/goal/get.
The control was stopped after four seconds; it was not left sleeping for five days. The alternate case changed only the error contract to error.type=error.code="usage_limit_reached", retained HTTP 429, the policy message and the same Retry-After, and supplied error.resets_at in epoch seconds. No plan_type was supplied.
| Same Codex binary, separate isolated homes | Original contract | Candidate terminal contract |
|---|---|---|
| Requests to mock | 1 | 1 |
| Error notification | responseStreamDisconnected, HTTP 429 |
usageLimitExceeded |
willRetry |
true |
false |
turn/completed |
Not emitted during the bounded observation | Emitted, status failed |
| Goal status | active |
usageLimited |
| Time | Still waiting at 4.37 s; interrupted by test | Entire harness completed in 0.24 s |
Both mock listeners and subprocesses were stopped after the tests. No real provider, production credentials or production Goal state was used. This validates the app-server/client contract; I did not run a second desktop/mobile visual test of the candidate.
Version
Runtime: @bitkyc08/opencodex 2.83.0; Bun 1.4.2.
Client: bundled codex-cli 0.162.0-alpha.17.2.
Also inspected the official v2.84.0 source: the affected cooldownErrorResponse() function is byte-for-byte identical. I have not run a production 2.84.0 instance.
Operating system
macOS 27.0.1, arm64
Provider and model
openai / gpt-6-astra, reasoning effort xhigh in the observed task. The isolated reproduction also used the same model name, served only by a loopback mock.
Logs or error output
Relevant production log fields, with identifiers and unrelated headers removed. Times are 2026-10-13, UTC+08:00:
12:35:02 OpenCodex:
status=429 durationMs=25 usageStatus=unreported
errorCode=rate_limit_exceeded failureStage=headers-only
upstreamError="Codex main account is blocked by the main-account quota policy
(5h ≥ 90%, weekly ≥ 98%). Choose another account, wait for quota to reset,
or disable codexMainAccountHardLock in Settings."
12:35:02 Codex HTTP client:
POST http://127.0.0.1:10100/v1/responses
status=429 Too Many Requests
retry-after: 446329
12:35:02 codex_core::responses_retry:
stream disconnected - retrying sampling request
(1/5 in 446328.999896666s)...
retries=1 max_retries=5
sampling_error=exceeded retry limit, last status: 429 Too Many Requests
13:37:53 / 13:52:29 / 14:32:35:
active_turn_present=true active_turn_status=Some(InProgress)
16:51:39:
op: Interrupt
There were no additional proxy requests for this conversation between the local refusal and the interruption. This is a retry sleep, not an unfinished shell process or hours of model reasoning. The isolated client does emit a transient Reconnecting... 1/5 notification; the defect is not an assertion that no error event exists, but that no terminal usage-limit/Goal transition follows.
Screenshots and supporting files
Source references:
- v2.83.0 main-account error class
- v2.83.0 cooldownErrorResponse
- v2.84.0 cooldownErrorResponse, still unchanged
- Codex Responses retry loop — current source corroborates honoring the server deadline; the installed client's behavior was verified separately above.
The installed affected function was compared against official v2.83.0 and is byte-for-byte identical. Existing unrelated local customizations are therefore not needed to produce this error contract.
Tested narrow mitigation (not a complete pool-recovery fix): handle CodexMainAccountHardLockError before the generic rate-limit formatter, return terminal usage_limit_reached, and include resets_at only when a finite future reset is known. Preserve the quota guard, its thresholds and account-routing decisions. Do not shorten a real throttle's Retry-After or repeatedly resend into a quota refusal.
Candidate checks in isolation also covered missing, past and non-finite reset values, unchanged short throttles, unchanged generic long cooldowns, and unchanged credits/reserve error classes. The candidate is not deployed to the production proxy. Other quota-related classes may merit separate classification work; this report's verified mitigation is scoped to the observed main-account error contract. Pool-aware recovery scheduling and automatic Goal continuation remain unimplemented and unverified.
Redacted configuration
{
"providers": {
"openai": {
"baseUrl": "https://chatgpt.com/backend-api/codex",
"codexAccountMode": "pool"
}
}
}
The hard-lock flag and thresholds were absent from the actual config, so the native enabled/default policy applied. No protection threshold, credits consent or model was changed to run the candidate tests.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 5h 7m
- Merged PRs (30d)
- 617
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
[Bug]: ocx doctor reports a false history integrity failure when state_5.sqlite is WAL without -shmOpenbug cli
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug provider
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
account-pool enhancement needs-design proxy
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
lidge-jun/opencodex#6737 · 1 comment ·
Maintainers usually reply within 1 day
-
bug catalog provider tools
Difficulty 4/5 3-5 days Newbie friendliness 38/100
lidge-jun/opencodex#7049 · 1 comment ·
Maintainers usually reply within 1 day
-
account-pool bug platform service
Difficulty 4/5 3-5 days Newbie friendliness 58/100
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
ui(ListToolbar): search takes the full row on a phone instead of sharing it with the result countOpen
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
Maintainers usually reply within 1 day
-
priority:high
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
immich-app/immich#32426 ·
Maintainers usually reply within 1 day
-
documentation good first issue hacktoberfest help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
libredb/libredb-studio#1756 ·
Maintainers usually reply within 1 day