v2.3.3
August 13, 2026 · View on GitHub
A hardening release. Nine fixes across rate limiting, token refresh, streaming, and storage.
npm i -g codex-multi-auth
Nothing about routing, account selection, storage layout, or the normal auth flow changed — these all live in failure and concurrency paths.
The big one
A single bad response could bench an account for 31 years. Upstream retry-after values were honoured with no upper limit, and the setter only ever grew the window. So one hostile or buggy response — a seconds-vs-milliseconds mixup, an anti-abuse misfire, a retry-after-ms of 999999999999 — marked the account rate-limited for three decades, wrote it to disk, and never self-healed.
Because the lockout was per-account, the pool was never fully exhausted, so the stale-recovery guard never kicked in either. Retry and quota windows are now clamped to 7 days.
Multi-process fixes
These matter if more than one CLI or proxy instance shares an auth directory. Single-process users aren't affected.
A slow refresh could delete someone else's lock. The refresh lease wrote an owner payload but never read it back, and release deleted the lock unconditionally. If a refresh ran about as long as the lease TTL, its lease expired, a second process took the lock, and then the slow original deleted the new owner's lock — leaving two processes refreshing at once. Since refresh tokens are single-use, the loser could submit an already-consumed token and log the account out entirely. Locks now carry a per-owner nonce.
Freshly rotated tokens could be reverted. Saving discarded disk state and rewrote the whole pool from memory, so when one process refreshed an account and wrote a rotated token, a routine save from a long-lived proxy could last-writer-win and put the old one back — permanently breaking that account's next refresh. Saves now reconcile token material from disk.
Rate limits and forecasting
- A transient
429benched an account for hours. A blip with a 30-secondRetry-Afterwas being folded together with the healthy weekly window, producing a multi-hour deferral. Only genuinely exhausted windows count now. forecastrecommended the wrong account. It took the longest of both quota windows, so a healthy weekly window (7 days out) drowned out the 5-hour window that was actually binding and about to free up in seconds. It then sorted by that wait and preferred a strictly worse account.
Streaming
Upstream failures were reported to you as success. A mid-stream error event, or a terminal response.failed, still came back as HTTP 200. Downstream ran the success path, the guard that would have caught it threw and was swallowed, and the account was recorded as healthy with retry and rotation suppressed. A stream that opens 200 and ends without a successful response now routes to failure.
response.incomplete — hitting a token cap or a content filter — is deliberately not a failure. It carries a real partial answer, so it's delivered at 200.
The SSE parser required a space after data:. A spec-valid data:value parsed as zero events, which would silently degrade any response to "no final response" if upstream ever changed its formatting.
Storage and auth
- The V1→V3 migration threw away the migrated account bodies, so an account upgrading with an active rate limit was read as immediately available and could burst
429s. - The local token store renamed a temp file without flushing it first, so a crash could leave it truncated.
- OAuth
expires_inaccepted zero and negative values, minting an already-expired token and driving a tight refresh loop that consumed the single-use refresh token. - The log scrubber now masks this package's own
cma_local_…tokens alongside the JWT andsk-patterns it already caught.