Client or integration
Codex App
Area
Authentication and account pool
Summary
In Pool mode with the Codex app signed in as the main login and one stored account added through ocx account login openai, automatic placement never picks the main login. Every unpinned request goes to the stored account, whatever the usage numbers, the strategy or codexAccountPriorities say. Main only serves when it is pinned, or when no stored account can be selected.
The cause is in resolveCodexAuthContext (src/codex/auth-context.ts): nativeMainReadsForbidden is set whenever requestScopedMainCredential is true, and selection receives isMainAccountTokenLive: () => preserveRequestOwnedMainPin. That callback is true only when main is the effective pin with headroom, so for an unpinned request codexAccountUnusableReason("__main__") returns main_credential_unavailable, getEligiblePoolAccounts never lists main, and the strategy only compares the stored accounts. Main is reached afterwards through resolveCallerOwnedMainContext, but only as the fallback when nothing else is selectable.
The fence itself makes sense: a request that carries its own main bearer must not read or reconcile the stored auth.json. But that same bearer is exactly what would be forwarded if main were selected, so excluding main from the comparison discards the usage, priority and reset ordering the operator configured. With one stored sibling the pool degrades to "stored account until it cannot serve, then main". #3996 (fixed) made the caller-owned fallback reachable in more cases; this is about main taking part in the ordering before that fallback.
Expected: for a request carrying a forwardable main bearer, main takes part in selection as a candidate served from that bearer, ordered by the same usage, priority and strategy as the stored accounts, without touching the stored main credential.
Reproduction
- Canonical
openai provider in Pool mode (codexAccountMode: "pool", authMode: "forward"). Codex app signed in as the main login. Add one stored account: ocx account login openai --device.
- Make the numbers favour main: main at lower weekly usage than the stored account (here 46% against 75%). Clear any pin:
PUT /api/codex-auth/active with {"accountId": null}; GET /api/codex-auth/active then shows activeCodexAccountId and pinnedAccountId both null.
- Send fresh turns from the app. I used
codex exec --ephemeral -c 'mcp_servers={}' "Reply with the single word yes." with a different word each time so every turn is a new conversation, then read accountLogLabel on the newest row of GET /api/logs?limit=1.
- Observed: every turn is served by the stored account. Same result with
accountPoolStrategy quota and reset-first, with ocx account priority openai main first, on gpt-6-astra and on gpt-5.5.
- Control:
PUT /api/codex-auth/active {"accountId": "__main__"} and the next turn is served by main. Pause the stored account and the next unpinned turn is served by main. GET /api/codex-auth/accounts?refresh=1 shows main with healthLabel: "Healthy", quotaRefresh: {"status": "ok"}, needsReauth: false, so it is not a credential or health problem.
Version
2.58.0; the same path is present in current dev (src/codex/auth-context.ts, nativeMainReadsForbidden).
Operating system
macOS 26.0 (Darwin 25.3)
Provider and model
openai, canonical ChatGPT backend; gpt-6-astra and gpt-5.5
Logs or error output
No error is produced. GET /api/logs?limit=1 after each unpinned turn carries the stored account's accountLogLabel; after a pinned-main turn the field is absent, which is how main is reported.
Screenshots and supporting files
No response
Redacted configuration
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"authMode": "forward",
"codexAccountMode": "pool"
}
One stored pool account. accountPoolStrategy tried as quota and reset-first, autoSwitchThreshold 80, codexAccountPriorities with main at +2.
Checks
Client or integration
Codex App
Area
Authentication and account pool
Summary
In Pool mode with the Codex app signed in as the main login and one stored account added through
ocx account login openai, automatic placement never picks the main login. Every unpinned request goes to the stored account, whatever the usage numbers, the strategy orcodexAccountPrioritiessay. Main only serves when it is pinned, or when no stored account can be selected.The cause is in
resolveCodexAuthContext(src/codex/auth-context.ts):nativeMainReadsForbiddenis set wheneverrequestScopedMainCredentialis true, and selection receivesisMainAccountTokenLive: () => preserveRequestOwnedMainPin. That callback is true only when main is the effective pin with headroom, so for an unpinned requestcodexAccountUnusableReason("__main__")returnsmain_credential_unavailable,getEligiblePoolAccountsnever lists main, and the strategy only compares the stored accounts. Main is reached afterwards throughresolveCallerOwnedMainContext, but only as the fallback when nothing else is selectable.The fence itself makes sense: a request that carries its own main bearer must not read or reconcile the stored
auth.json. But that same bearer is exactly what would be forwarded if main were selected, so excluding main from the comparison discards the usage, priority and reset ordering the operator configured. With one stored sibling the pool degrades to "stored account until it cannot serve, then main". #3996 (fixed) made the caller-owned fallback reachable in more cases; this is about main taking part in the ordering before that fallback.Expected: for a request carrying a forwardable main bearer, main takes part in selection as a candidate served from that bearer, ordered by the same usage, priority and strategy as the stored accounts, without touching the stored main credential.
Reproduction
openaiprovider in Pool mode (codexAccountMode: "pool",authMode: "forward"). Codex app signed in as the main login. Add one stored account:ocx account login openai --device.PUT /api/codex-auth/activewith{"accountId": null};GET /api/codex-auth/activethen showsactiveCodexAccountIdandpinnedAccountIdbothnull.codex exec --ephemeral -c 'mcp_servers={}' "Reply with the single word yes."with a different word each time so every turn is a new conversation, then readaccountLogLabelon the newest row ofGET /api/logs?limit=1.accountPoolStrategyquotaandreset-first, withocx account priority openai main first, ongpt-6-astraand ongpt-5.5.PUT /api/codex-auth/active {"accountId": "__main__"}and the next turn is served by main. Pause the stored account and the next unpinned turn is served by main.GET /api/codex-auth/accounts?refresh=1shows main withhealthLabel: "Healthy",quotaRefresh: {"status": "ok"},needsReauth: false, so it is not a credential or health problem.Version
2.58.0; the same path is present in current
dev(src/codex/auth-context.ts,nativeMainReadsForbidden).Operating system
macOS 26.0 (Darwin 25.3)
Provider and model
openai, canonical ChatGPT backend; gpt-6-astra and gpt-5.5
Logs or error output
No error is produced.
GET /api/logs?limit=1after each unpinned turn carries the stored account'saccountLogLabel; after a pinned-main turn the field is absent, which is how main is reported.Screenshots and supporting files
No response
Redacted configuration
One stored pool account.
accountPoolStrategytried asquotaandreset-first,autoSwitchThreshold80,codexAccountPrioritieswith main at+2.Checks