Repository navigation
Pool routing never selects the main login automatically when the request carries that login's own bearer #5019
Description
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
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. - 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/activewith{"accountId": null};GET /api/codex-auth/activethen showsactiveCodexAccountIdandpinnedAccountIdbothnull. - 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 readaccountLogLabelon the newest row ofGET /api/logs?limit=1. - Observed: every turn is served by the stored account. Same result with
accountPoolStrategyquotaandreset-first, withocx account priority openai main first, ongpt-6-astraand ongpt-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=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=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
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
fix(codex): let a request-owned main bearer take part in pool ordering
lidge-jun commentedon Sep 18, 2026
Confirmed at the tip, and the fix is a different shape than the report implies
Assessed against
devat11bc4f708c, then implemented and put through hosted CI. Static sourceonly for the analysis; no local verification was run in this lane.
The report's causal account is correct.
resolveCodexAuthContextpasses selectionisMainAccountTokenLive: () => preserveRequestOwnedMainPinwhenever the request carries main's ownforwardable bearer (
src/codex/auth-context.ts:1040). That value answers a different question, sofor an unpinned request
codexAccountUnusableReasonreturnsmain_credential_unavailable(
src/codex/account-usability.ts:74),getEligiblePoolAccountsnever lists main(
src/codex/routing/selection.ts:181), and every strategy compares only the stored accounts. Mainis reached afterwards only through
resolveCallerOwnedMainContextat:1101and:1241, both ofwhich are no-candidate fallbacks. None of #4921, #4676 or #4935 changed this: #4676 touches no file
on this path, and #4935's cohort keying never applies here because
poolStateEligibleexcludes arequest-owned credential from Pool state entirely.
But flipping that predicate is not sufficient, and the reason is worth recording. I implemented
it in #5024 and the hosted suite showed that the exclusion was doing three jobs rather than one.
Beyond keeping main out of the comparison, it was also keeping main out of:
__main__intoconfig.activeCodexAccountId—codex-auth-context.test.ts:1351catches itexactly. A credential that exists for one request must not move durable Pool state, and nothing
had to fence that write while main could not be selected.
callerIsCooledPoolAccountis asked of the selectedaccount, which was sufficient only because the cooled account always was the selection. Once
main is selectable, a caller whose own credential is a cooled subscription gets served as main,
resurrecting the cooldown it is inside. Three cases in the same file cover this.
So the expected behavior in this report is right, and the implementation it suggests is not: a
single liveness answer can make main a candidate but cannot keep that candidacy out of Pool state.
The shape that fits: ask the existing non-persisting selector,
previewCodexAccountForRequest, whether main wins the ordering for a request-owned bearer. If itdoes, serve
resolveCallerOwnedMainContext(). If it does not, leave main ineligible for the realselection, so the cursor, the cooldown identity check and the probe lease keep behaving exactly as
they do today. That answers the ordering question the report is about and writes nothing.
Account-gated models stay out of it either way: main's roster is discovered from the stored
credential this request may not read, so candidacy alone cannot produce a grant. That set currently
holds one non-flagship slug (
ACCOUNT_GATED_NATIVE_OPENAI_MODELS), so it does not affect the modelsin the report.
#5024 carries the confirmed diagnosis and the failing evidence. It is not ready to merge, and I did
not relax the three cooldown tests to make it green — they are the invariant, not the obstacle.
fix(codex): let a request-owned main bearer take part in pool ordering
fix(codex): let a request-owned main bearer take part in pool ordering (
lidge-jun commentedon Sep 23, 2026
Landed on dev in ac78d95 via #5024: a request carrying the main login's own bearer now lets main take part in pool ordering, so usage, priority and reset ordering apply to it instead of the pool comparing only the stored accounts. Merges into dev do not auto-close issues, so closing manually; it ships with the next release cut from dev. Please reopen if it still reproduces there.