Skip to content

Releases: ndycode/codex-multi-auth

v2.3.3

19 Jun 10:03
826dc07

Choose a tag to compare

Patch release: security and durability hardening from a deep stress audit of the rotation, persistence, and SSE failure-handling paths (#617, #618, #619). No feature changes; routing, account-selection, and the normal auth flow are unchanged.

Install: npm i -g codex-multi-auth


Runtime Rotation / Recovery

Bugfixes

  • Fixed an unbounded rate-limit window that could wedge an account unavailable for years. A 429 (or a 2xx carrying x-codex-*-reset-* headers) was honored using the upstream retry-after/reset value with no upper clamp, and the setter only lower-clamped while growing resetAt monotonically — so a single hostile or buggy response (a seconds-vs-milliseconds confusion, an anti-abuse misfire, a retry-after-ms of 999999999999) marked the account rate-limited for ~31 years, persisted that to disk, and never self-healed. Because the lockout is per-account the pool was never fully exhausted, so the stale-recovery guard never fired either. Retry and quota windows are now clamped to MAX_RATE_LIMIT_DELAY_MS (7 days), applied centrally in markRateLimitedWithReason and again at source in getQuotaNearExhaustionWaitMs, matching the clamp the legacy reactive fetch path already enforced (#617).
  • Fixed the refresh lease deleting a lock it no longer owned. The lease wrote a pid/acquiredAt payload but never read it back at release, and release() called safeUnlink(lockPath) unconditionally. If an owner's refresh ran about as long as the lease TTL, its lease expired, a second process stole the lock, and the slow owner then deleted the new owner's lock on completion — leaving two concurrent refreshers. Because the OAuth refresh token rotates per refresh, the losing process could submit an already-consumed token and log the account out until re-login. The lock now carries a per-owner nonce and release() only unlinks when the on-disk nonce still matches; a lock written before this change (no nonce) keeps the prior best-effort behavior. This is scoped to deployments running more than one CLI/proxy instance against a shared auth directory (#617).
  • Fixed a cross-process clobber of freshly-rotated refresh tokens. saveToDisk discarded the disk-loaded state and re-serialized the entire in-memory pool, so when a second process refreshed an account and wrote a rotated single-use token, a routine save from a long-lived proxy (cooldown, rate-limit, near-quota refund) could last-writer-win and revert it — permanently breaking that account's next refresh. The save now reconciles per-account token material from disk under the storage lock, adopting a strictly-newer on-disk token; the refresh-commit path still wins with its own fresher token (#617).

Quota / Forecast

Bugfixes

  • Fixed a transient 429 benching an account for the full deferral cap. markRateLimited folded the existing weekly secondary reset (normally ~7 days out on a healthy 200 snapshot) into the primary window via max(...), so a blip with a 30–60s Retry-After produced a multi-hour deferral and the account was rotated away for the full 2h cap. The 429 path now treats only genuinely-exhausted windows (used ≥ 100%, or a window with no usage gauge) as rate-limit windows, in both markRateLimited and getDeferral; the documented "longest active reset window" behavior for real rate-limit windows is unchanged (#617).
  • Fixed the live-quota forecast overstating the wait and inverting the recommendation. getLiveQuotaWaitMs took a blind max of both quota windows, so a healthy weekly secondary (~7 days) dominated a binding 5h primary that frees in seconds, and recommendForecastAccount — which sorts ascending by wait — then preferred a strictly-worse account and displayed a wait wrong by orders of magnitude. Under usage pressure it now filters to exhausted windows, mirroring the quota-cache path; a 429 still honors every active window (#617).

Request / SSE Data Path

Bugfixes

  • Fixed upstream SSE failures being reported to the client as success. A mid-stream {"type":"error"} event, or a terminal response.failed event, left convertSseToJson returning the raw SSE text at HTTP 200; downstream this ran the success path, the empty-response guard's JSON.parse threw on the SSE body and was swallowed, and the account was recorded as a success with rotation and retry suppressed. A stream that opens 200 but ends without a successful final response now resolves to a synthesized non-2xx so the caller routes to failure, while a stream that simply yields no events without an error is still passed through so the empty-response retry path is preserved (#617, #618).
  • Fixed the SSE parser requiring a trailing space after data:. A spec-valid data:value line (no space) parsed as zero events, silently degrading the response to "no final response" on any upstream or proxy formatting change. The parser now accepts data: with optional whitespace (#617).

Behavior

  • response.incomplete (hitting max_output_tokens or a content filter) is treated as a normal early stop, not a failure. It carries a final response object whose partial output is the answer, so it is delivered at HTTP 200 and counts as a healthy account — distinct from the response.failed path above, which routes to failure (#618).

Storage / Auth / Logging

Bugfixes

  • Fixed the V1→V3 storage migration discarding the migrated account bodies. normalizeAccountStorage rebuilt the account list from the raw V1 objects rather than the migration output, dropping migrateV1ToV3's scalar rateLimitResetTime → map rateLimitResetTimes conversion — so a rate-limited account upgrading from V1 was read with no reset times, treated as immediately available, and could burst 429s. The account list is now built from the migrated storage (#619).
  • Fixed a durability gap in the local-client-token store. The store wrote a temp file and renamed it with no fsync in between, so a crash or power-loss after the rename could leave it truncated. The temp file is now flushed with fsync before the rename, matching the durable-write pattern already used by the app-bind and first-run writers (#619).
  • Fixed OAuth expires_in (and the internal expires) accepting any number. A zero or negative value minted an already-expired token, which drove a tight refresh loop that consumed the single-use refresh token; the value now must be a positive integer or it fails schema validation (#619).
  • Hardened the free-text log scrubber to mask this package's own local bearer tokens (cma_local_…) alongside the existing JWT, long-hex, sk-, and Bearer patterns. Structured logging already masks by key and the OAuth path is scrubbed separately; this closes the last-line-of-defense gap for the project's own token shape (#619).

Testing

Improvements

  • Added regression coverage for every fix: the 7-day retry-after clamp (including self-heal once the window elapses) and the at-source quota clamp; the lease ownership nonce, proving a slow owner does not delete a stolen lock; the cross-process token-clobber reconciliation; the transient-429 deferral bound alongside the preserved "longest active reset window" semantics; the forecast exhausted-window filter and the no-longer-inverted recommendation; SSE error/response.failed → non-2xx, response.incomplete200 with partial output, and data: parsing without a trailing space; the V1→V3 rateLimitResetTimes preservation; the OAuth expires_in positive-integer bound; and the cma_local log-masking pattern.
  • Updated four existing tests that asserted the prior behavior (SSE error events returning a raw HTTP 200) to assert the corrected failure routing.

Notes

  • Patch release published under the latest dist-tag (npm i -g codex-multi-auth).
  • No runtime-rotation routing, account-selection, storage layout, or normal auth-flow behavior changed; the fixes harden failure, concurrency, and edge-case paths.
  • The multi-process fixes (lease ownership, token reconciliation) matter most when more than one CLI/proxy instance shares an auth directory; single-process usage is unaffected by those races.

v2.3.2

16 Jun 14:55

Choose a tag to compare

Patch release: self-healing recovery for an orphaned runtime-proxy app-bind (#614, #615). No runtime-rotation, storage, or auth behavior changed.

Install: npm i -g codex-multi-auth


Runtime Rotation / App Bind

Bugfixes

  • Fixed an orphaned app-bind state that could leave ~/.codex/config.toml pointing at the codex-multi-auth-runtime-proxy provider with no way to recover via the CLI. When app-bind rewrote the config but its state/backup files were later lost (cleanup, partial unbind, a marker-less re-run of first-run setup, or a crash), the config stayed bound while getAppBindStatus and rotation unbind-app — which inferred "bound" purely from the app-bind state files — reported "not configured" and refused to act. The official Codex CLI/Desktop was then routed to a dead proxy port with no automated fix. unbindCodexAppRuntimeRotation now self-heals: when there is no backup and no state but config.toml is still bound, it strips the proxy provider block, restores the top-level model_provider (falling back to openai when no original backup exists), and reports the recovery. getAppBindStatus now derives bound from the config when no state file is present and exposes an unmanagedBind flag, and rotation status surfaces "bound but unmanaged" with the unbind-app remedy instead of "not configured" (#614).
  • Fixed a duplicate model_provider key in the no-backup recovery path. A half-orphaned config (proxy block present but the top-level model_provider already pointing at a real provider) would have a second model_provider line spliced in during restore, producing invalid TOML that Codex refuses to parse. The shared restore now only inserts the original line when no top-level model_provider exists; an existing line is left untouched.

Testing

Improvements

  • Added regression coverage for the recovery path: detection of a bound config from either the top-level provider or the proxy block (including stray-mention and empty-config cases); no-backup restore across full-orphan, half-orphan, block-only, custom-default-provider, and CRLF variants; disable_response_storage cleanup; unmanagedBind status detection; and integration-level self-heal through unbindCodexAppRuntimeRotation for both the full-orphan and half-orphan cases.

Notes

  • Patch release published under the latest dist-tag (npm i -g codex-multi-auth).
  • No runtime-rotation routing, account-selection, storage, or auth behavior changed; the normal backed-up bind/unbind path is unchanged.
  • If you previously hit the orphaned-bind state, run codex-multi-auth rotation unbind-app to restore your config (it now works even without a saved backup).

v2.3.0

15 Jun 09:41
65b16e9

Choose a tag to compare

Runtime Rotation

Bugfixes

  • Fixed a stale-runtime recovery deadlock that returned a permanent 503 "All managed Codex accounts are temporarily unavailable for this runtime request." even when accounts were healthy and doctor passed. Per-account transient state (coolingDownUntil, cooldownReason, rateLimitResetTimes) is serialized into the V3 snapshot, so recoverStaleRuntimeState's loadFromDisk() restored the same state that had wedged the pool, while the recovery guard refused to run whenever any account's skip reason was "rate-limited" or "cooling-down*". The guard now suppresses recovery only on "policy-blocked" (external, unchanged by a reload), and recoverStaleRuntimeState calls AccountManager.clearAccountTransientState() then flushPendingSave() before publishing the reloaded manager, so the cleared snapshot survives a restart within the debounce window. The two halves are coupled — each is pinned by a regression test that fails if the other is reverted (#606, #607).
  • Fixed the missing-accountId cooldown branch in runRotationLoop not persisting its mutation. When resolveAccountId returned null the branch called markAccountCoolingDown but — unlike every sibling cooldown branch (network-error, 429, server-error, 401 invalidation) — never called saveToDiskDebounced(), so a restart inside the 30s window dropped the cooldown and immediately re-selected the still-broken account. Added the missing persist (#608).
  • Fixed the short-retry 429 path in the runtime fetch loop not persisting its rate-limit window. The branch mutated the disk-serialized rateLimitResetTimes via markRateLimitedWithReason, then slept and retried without a saveToDiskDebounced(), unlike the full-rotation branch beside it; a crash during the retry sleep lost the reset time. Added the missing persist (#609).

Testing

Improvements

  • Added regression coverage for all three durability fixes: clearAccountTransientState unit tests (cooldown clear, rate-limit clear incl. future windows, mixed state, no-op on empty pool, flush-backed persistence); all-cooling-down pool recovers to 200 while policy-blocked pools still suppress recovery; missing-accountId and short-retry branches each assert saveToDiskDebounced is scheduled. Each fix's test fails if its source change is reverted (verified by mutation).

Notes

  • Stable release published under the latest dist-tag (npm i -g codex-multi-auth).
  • All three fixes share one root-cause class: transient account state is persisted to disk, so any path that mutates it must schedule a write or a restart silently drops it. All mutation sites were audited; these were the gaps.
  • The codex-multi-auth rotation reset-rate-limits command remains available as a manual escape hatch.
  • Promotes the 2.3.0-beta line to stable; all fixes from 2.3.0-beta.12.3.0-beta.3 are included.

v2.3.0-beta.3

11 Jun 08:17

Choose a tag to compare

v2.3.0-beta.3 Pre-release
Pre-release

Runtime Rotation

Bugfixes

  • Fixed stream forwarding stalling indefinitely for slow clients. forwardStreamingResponse now checks the return value of res.write() and awaits drain before reading the next upstream chunk, preventing unbounded in-process buffering when the client socket falls behind.

Improvements

  • Converted the two startup guards in startRuntimeRotationProxy from bare Error throws to CodexValidationError with machine-readable field/expected/context metadata. Error messages are byte-identical; callers can now branch on instanceof CodexValidationError and stable field names instead of message text (audit §4.3, #586).

Storage

Bugfixes

  • Fixed multi-tier account deduplication. deduplicateAccountsByIdentity now runs fixpoint iteration: a single pass was not enough when a newest-wins merge could install an account that itself duplicated an earlier survivor through a different identity tier (e.g. an email-tier merge installs an account whose accountId + refreshToken already matches an earlier entry). The wrapper now loops until the array is stable; every pass strictly shrinks it by at least one entry, so it terminates in at most accounts.length passes.
  • Added vi.restoreAllMocks() to storage.test.ts afterEach to prevent a failing test's leaked fs spy from cascading into every subsequent storage test in the same worker.

Improvements

  • Migrated the last two hand-rolled retry loops to the shared withRetry helper in lib/fs-retry.ts: the temp→final account-save rename (storage.ts) and the config env-path CAS loop (config.ts). Inter-attempt delay schedules are unchanged; only the wasted trailing sleep after a final failure is removed.
  • Converted savePluginConfig's "unreadable config file" abort from a bare Error to a typed StorageError with code: "UNREADABLE" and the file path. Callers can now branch on instanceof StorageError instead of message text (#588, audit §4.3).

Security

Improvements

  • All atomic write helpers now use crypto.randomBytes instead of Math.random() for staging-path nonces, preventing a local attacker from predicting the next staging path (#517).

Code Quality

Improvements

  • Removed 852 lines of dead code: seven orphaned modules with no live importers deleted (#554, #558).
  • Pruned unused exports and types flagged by knip; added knip.jsonc config for ongoing dead-code analysis (#555, #556, #557).
  • isRetryableStorageWriteError, copyDashboardSettingValue, mergeDashboardSettingsForKeys, and DEFAULT_STATUSLINE_FIELDS exported from their respective modules for direct test access.
  • Synced plugin manifest and AGENTS.md package-version claim to v2.3.0-beta.3.

Testing

Improvements

  • 20 new direct test suites covering: login-oauth, login-menu actions/flow/data, persist-selected, health-check, forecast-report-shared, settings write-queue, rotation selection/state/token-refresh, auth-menu builder, model-fallback property, write-queue property, rate-limit helpers, usage-ledger redaction, settings preview builders, and settings-hub shared helpers.
  • Property-based test suite for deduplicateAccountsByIdentity: covers order-independence and convergence across all permutations using fast-check.
  • shouldRetryFileOperation, fs-retry, and temp-path covered with new unit suites.

Notes

  • Prerelease published under the beta dist-tag (npm i -g codex-multi-auth@beta).
  • The #509 sequential drain-first feature and all fixes from 2.3.0-beta.2 are included.

v2.3.0-beta.2

10 Jun 17:43

Choose a tag to compare

v2.3.0-beta.2 Pre-release
Pre-release

Runtime Rotation

Bugfixes

  • Fixed the sequential drain-first pointer advancing during a within-request fallback in legacy routing mode. persistRuntimeActiveAccount called markSwitchedLocked unconditionally when routingMutex !== "enabled", including when schedulingStrategy: "sequential" was set; a transient token-bucket fallback to a secondary account would permanently move the primary and break the #509 drain-first invariant. The same schedulingStrategy !== "sequential" guard already present in the enabled-mutex branch now applies to the legacy branch too.
  • Fixed ensureFreshAccessToken forwarding an expired access token when commitRefreshedAuthOnce returned null (account not found in storage after persist). The fallback used updatedAccount.access, which was the original stale token, causing a downstream 401 and incorrectly triggering invalidation-cooldown logic. The function now uses refreshResult.access (the just-issued token) on the null-commit path while preserving the committed account's stored token on the success path (required for the dedup-commit case where two concurrent callers share one commit).

Storage

Bugfixes

  • Fixed normalizeFlaggedStorage silently dropping workspaces, currentWorkspaceIndex, accessToken, and expiresAt from flagged accounts. The function built a hardcoded field list that omitted these fields, defeating the Zod schema's .passthrough() intent; multi-workspace accounts permanently lost their workspace list and active-workspace index after a flag→restore round-trip. All four fields are now preserved.
  • Fixed refreshQuotaCacheForMenu wiping all other accounts' quota data when a transient disk failure occurred during cache reload. The rebase-merge block had a dead catch because loadQuotaCache() never throws — it returns empty maps on any read failure. When the persisted cache came back empty, only this run's probed entries were written back, discarding every other account's still-valid quota data. The empty-load case is now detected explicitly and falls back to the full in-memory snapshot.

Security

Hardening

  • All atomic write helpers (storage.ts, recovery/storage.ts, quota-cache.ts, unified-settings.ts, and twelve other call sites) now use crypto.randomBytes for staging-path nonces instead of Math.random(), preventing a local attacker who can observe one staged path from predicting the next one (#517).
  • All GitHub Actions workflow steps are pinned to exact commit SHAs rather than floating version tags, hardening against supply-chain tag drift (#519).
  • Upstream response headers matching x-codex-multi-auth-account-* are now blocked by prefix match rather than an allowlist; a future header added under that namespace is blocked by default rather than leaking until someone extends a list (#546).
  • Fixed withTimeout rejecting after calling onTimeout: cancelling a stream reader in onTimeout can settle the raced promise with a clean done: true, and a settlement enqueued ahead of the rejection would win the race, turning a stall into a silent success. Reject is now issued before onTimeout (#546).

Refactoring

Improvements

  • Decomposed lib/codex-manager.ts across four phases: login control loop, OAuth machinery, command registry, and formatter modules extracted into lib/codex-manager/ submodules. The file shrinks from 2,266 lines to 690 lines (-82%) (#525, #535, #540, #547).
  • Decomposed lib/runtime-rotation-proxy.ts across two phases: rate-limit decision logic, stream-failover runtime, and rotation-proxy closure state extracted into lib/request/ and lib/runtime/ submodules (#532, #548).
  • Eliminated all detected import cycles; eslint-plugin-import-x no-cycle rule is now enforced in CI so regressions are caught at commit time (#541).
  • Replaced local isRecord guards in runtime-current-account.ts, codex-manager/commands/rotation.ts, and refresh-lease.ts with the canonical lib/utils.ts version. The local copies accepted arrays (typeof v === "object" && v !== null); the canonical version correctly rejects them (#544, #545).

CLI & Tests

Improvements

  • Added shared cli-test-fixtures mock factory infrastructure; all manager test suites now use a consistent set of factories, removing per-suite boilerplate (#537, #539, #550).
  • Pinned the machine-readable JSON output contract (status, report, doctor, forecast, why-selected) with snapshot tests that catch shape regressions (#533).

Build & Packaging

Improvements

  • Stripped JS source maps from the published package; declaration maps are kept for go-to-definition. Reduces published size (#527).
  • Pinned @types/node to the supported runtime floor major to prevent silent type drift from newer Node API additions (#528).
  • Raised engines.node to >=18.17.0, reflecting the actual tested minimum and aligning with undici@6 and the node18-smoke CI job (#518).
  • config/schema/config.schema.json is now generated from the Zod schema with a byte-exact drift-guard test; the schema and its source of truth can never silently diverge (#536).

Notes

  • Prerelease published under the beta dist-tag (npm i -g codex-multi-auth@beta).
  • The #509 sequential drain-first feature from 2.3.0-beta.0 is unchanged; the pointer-corruption bug above is now fixed.

v2.3.0-beta.1

07 Jun 09:29
d8306d5

Choose a tag to compare

v2.3.0-beta.1 Pre-release
Pre-release

Account Login

Bugfixes

  • Stopped codex-multi-auth login from always printing Added account when a
    same-email / different-workspace login folded onto an existing saved entry.
    The CLI login path used local copies of resolveAccountSelection and
    persistAccountPool in lib/codex-manager.ts that had drifted from the
    workspace-aware versions in lib/runtime/ (added for #491 and used only by
    the runtime proxy); the CLI copies never persisted workspaces and
    unconditionally reported Added account. The login flow now reports the real
    outcome — Added account, Updated existing account, or
    Rebound workspace for existing account — based on whether the write
    inserted a new entry, refreshed an existing one, or surfaced a
    previously-untracked workspace (issue #512).
  • Persisted token-derived workspaces on the saved account so
    codex-multi-auth workspace <account> is usable after a same-email
    multi-workspace login. Rows no longer save with workspaces: null;
    per-workspace enabled/disabledAt state is preserved across re-logins, and
    the explicit login --org <id> binding now tracks workspaces too (previously
    the override path returned before workspace discovery).
  • Classified the first workspace-aware re-login of a pre-#491 account (one with
    no tracked workspaces yet) as Updated existing account rather than
    Rebound workspace, so quiet first-time enrichment is not mislabeled as a
    rebind.

Manual sign-in

  • Surfaced real validation errors from codex-multi-auth login --manual
    instead of reporting every failure as Cancelled.. The manual callback
    reader returned null for a genuine user cancel, a callback URL missing the
    code/state parameter, and an OAuth state mismatch alike, and the caller
    treated all three as a cancellation. A new pure classifier
    (classifyManualCallbackInput) distinguishes code / cancelled /
    invalid / state-mismatch; invalid and state-mismatch now exit non-zero
    with a specific, actionable message (callbackInvalid /
    callbackStateMismatch), while a genuine cancellation is unchanged
    (issue #512 follow-up).

Release Hygiene

Tests

  • Extracted the account-pool fold (dedup → insert/update/rebound →
    workspace-tracking → active-index) into pure helpers
    (applyAccountPoolResults, buildInsertedAccount, buildUpdatedAccount,
    mergeAccountWorkspaces, resolveCurrentWorkspaceIndex) and covered them with
    unit tests, including an end-to-end reproduction of the same-email
    multi-workspace scenario driven through the real findMatchingAccountIndex
    dedup strategy.
  • Added CLI regressions: login --org <id> persists workspace tracking, a
    mismatched manual-callback state exits non-zero with the state-mismatch
    message, and a malformed manual-callback URL exits non-zero with the
    callbackInvalid message — none of which persist an account.
  • Full classifier coverage for the manual-callback contract, including the
    reporter's "pasted a localhost callback URL but still saw Cancelled" case.

Notes

  • Prerelease published under the beta dist-tag
    (npm i -g codex-multi-auth@beta). This is a bugfix beta on the 2.3.0 line;
    the #509 sequential drain-first feature from 2.3.0-beta.0 is unchanged.

v2.3.0-beta.0

04 Jun 13:06

Choose a tag to compare

v2.3.0-beta.0 Pre-release
Pre-release

Runtime Rotation

Features

  • Sequential / drain-first account scheduling (opt-in). A new
    schedulingStrategy setting (CODEX_AUTH_SCHEDULING_STRATEGY /
    schedulingStrategy, default hybrid) adds a sequential mode that drains one
    account fully before moving to the next, instead of spreading load across the
    pool. Set it to sequential to keep every new request on the current active
    account until that account is exhausted (rate-limited, cooling down, circuit-open,
    or disabled), then advance to the next usable account. When an earlier account's
    quota window recovers it reclaims the active slot on the next forward scan, so the
    pool's quota windows stagger over time rather than resetting together — longer
    uninterrupted sessions across a multi-account pool (issue #509).
  • Default behavior is unchanged. hybrid remains the default and keeps the
    existing weighted health/token/freshness selection plus per-session affinity.
    sequential is fully opt-in; pools that do not set it behave exactly as before.
  • Manual pin still wins. A switch <n> pin overrides scheduling in both modes.
    In sequential mode per-session affinity is intentionally bypassed — every
    request follows the single active account, not a per-chat sticky account.

Release Hygiene

Tests

  • Selector coverage for the drain-first path: sticky-while-usable, advance-on-
    exhaustion, wrap-to-recovered-earlier-account, returns-null when the whole pool is
    exhausted, cooldown/circuit-open/disabled failover, per-family cursor isolation,
    and the policy-blocked-anchor guard.
  • Proxy-level coverage: affinity is ignored, manual pin takes precedence, the active
    pointer advances only on true exhaustion (not on a transient attempted-this-request
    skip), and the mode survives the routing-mutex select+commit path without double-
    advancing the cursor.
  • Config coverage for schedulingStrategy: default, explicit value, env override in
    both directions, and invalid env/persisted values falling back safely.

Notes

  • Prerelease published under the beta dist-tag
    (npm i -g codex-multi-auth@beta). Whether drain-first delivers longer
    uninterrupted sessions depends on each pool's real quota-window timing, which is
    why this ships as a beta for validation on real accounts before a stable cut.

v2.2.2

03 Jun 16:14

Choose a tag to compare

Runtime Rotation

Bugfixes

  • Cleared an account's persisted runtime skip reason on its next successful request, so a reason with no time-based expiry (notably token-exhausted) no longer lingers in accountSkipReasons and keeps the forecast reporting a working account as unavailable.
  • Added recordRuntimeAccountRecovery(index) on the proxy success path: it removes the account's entry from accountSkipReasons and lastPoolExhaustionSkipReasons, is a no-op when nothing is recorded, and ignores non-integer or negative indices.

Quota & Forecast

Bugfixes

  • Stopped forecast --live marking working accounts as unavailable from a stale runtime overlay that persisted a skip reason after its window expired but was never cleared on a subsequent successful request.
  • Ignored stale time-bounded overlay reasons by cross-referencing disk state: rate-limited when getRateLimitResetTimeForFamily finds no active reset (including model-scoped keys like codex:5h), and cooling-down:... when coolingDownUntil is absent or elapsed.
  • Left non-time-bounded reasons (circuit-open, token-exhausted, policy-blocked) applied as-is by the forecast; these are cleared at the source on a successful request instead (see Runtime Rotation).
  • Aligned doctor's forecast-runtime-alignment check, which shares the forecast evaluation, so the same stale state no longer raises a spurious warning.

v2.2.1

03 Jun 12:36

Choose a tag to compare

Launcher

Bugfixes

  • Rewrote mcodex as a Node bin (scripts/mcodex.js); the old bash script shipped as a Windows bin died with HCS_E_SERVICE_NOT_AVAILABLE when a WSL stub shadowed git-bash. Zero bash dependency on the default path; tmux/watch invoked as argv arrays; graceful degrade when absent.
  • Canonicalized the direct-run gate (realpath) so the launcher runs through an npm-created symlink bin.
  • Relayed SIGTERM/SIGINT to the spawned child so it is never orphaned.

Auth

Bugfixes

  • Isolated OAuth concurrent-login state in per-call closures instead of the shared http.Server instance, so parallel logins can't cross-bind callback code/state.
  • Serialized the local-client-token store's read-modify-write through its write queue and widened the rename retry set to EBUSY/EPERM/EAGAIN/ENOTEMPTY/EACCES; debounced lastUsedAt writes on the bearer-verify hot path.

Runtime Rotation

Bugfixes

  • Closed the routingMutex="enabled" selection race: selection and cursor commit now run in one reentrant mutex acquisition. Legacy mode unchanged.
  • Bucketed a model-less /codex/responses request into the codex family (CURRENT_CODEX_MODEL) instead of the general gpt-5.5 family, keeping rotation/cooldown/budget accounting correct.
  • Stripped inbound cookie / proxy-authorization on both egress paths; short-circuited per-request storage re-reads on unchanged mtime/size; check auth before path/method (401 before 404).

Storage

Bugfixes

  • Fixed a storage-transaction deadlock: flagged-storage recovery (backup restore + legacy-file migration) inside a held lock re-acquired the global mutex and wedged all later saves. Lock ownership is now tracked so recovery persists without re-locking.
  • Preserved pinnedAccountIndex/affinityGeneration through the combined transaction clone so a doctor restore no longer erases the manual pin.
  • Serialized the env-path config save under a cross-process file lock (owner token + compare-before-unlink) plus retried stat and mtime compare-and-swap.
  • Created secret directories 0o700; floored fractional indices and coerced NaN in clampIndex.

Quota & Forecast

Improvements

  • gpt-5.5 is now the default live/quota probe model, legacy fallback chain (gpt-5.4gpt-5.3-codexgpt-5.2-codexgpt-5-codex) preserved in one shared QUOTA_PROBE_MODEL_CHAIN.
  • Codex-unavailable accounts are labeled "signed in" not "working"; live check gained Codex available / signed in only / need re-login counters (! instead of ).

Bugfixes

  • Classified the normalized "model not currently available for this ChatGPT account" wording as an entitlement block across the probe/forecast/report/check surfaces.
  • Excluded policy-blocked and token-exhausted accounts from forecast recommendations.
  • Fixed status-tone precedence so a failed live check carrying a quota percentage renders red, not green.

CLI

Bugfixes

  • Hardened --model parsing on every parser (best, forecast, report, fix, integrations, models): a flag-like or whitespace-only value after --model/-m/--model= is rejected, not consumed.
  • Rejected non-integer workspace/switch indices instead of truncating; rejected a flag-like budget check key.
  • Shipped .codex-plugin/plugin.json in the package, enforced by the pack-budget check via an exact-file requirement.
  • Read the capability matrix under the entitlement key (matching the write path); clamped out-of-range quota percentages; made capability-policy eviction LRU.

v2.2.0

02 Jun 13:09
29c8b4f

Choose a tag to compare

Launcher

Improvements

  • New mcodex launcher (#500): launches Codex with a cached status line (model, reasoning effort, cwd, active account, quota usage, plan, cache age) printed before startup. mcodex --tmux runs inside tmux with mouse scrollback; --tmux --live-accounts adds a live codex-multi-auth list pane; --monitor is monitor-only.
  • The status line reads local cache/observability/account state and never calls OpenAI on launch; refreshes quota in the background only when stale (default 10 min, CODEX_MULTI_AUTH_STATUS_QUOTA_REFRESH_INTERVAL_MS), behind a lock. Resolves the per-project pool when perProjectAccounts is on and Codex CLI sync is off. Toggle with CODEX_MULTI_AUTH_STATUSLINE=0.

Hardening

  • Validated MCODEX_MONITOR_INTERVAL / MCODEX_TMUX_HISTORY_LIMIT as numeric before interpolating into watch/tmux commands (no shell injection).
  • --monitor / --live-accounts fail fast when watch is missing instead of spawning a broken pane; status path resolves ~ correctly on Windows and never blocks the event loop.

Runtime Rotation

Bugfixes

  • Bound the rotation proxy and local bridge loopback-only with no opt-out, and stopped forwarding inbound credentials (authorization, x-api-key, cookie, proxy-authorization) upstream; IPv6 loopback normalized for bind + base URL (#499).
  • Stripped inbound cookie / proxy-authorization on both egress paths and bounded the proxy's upstream error-body read (#503).

Storage

Bugfixes

  • SHA-256-verified cached Codex instructions: a tampered or unverified-legacy cache is never fast-path served and never drives a conditional 304 (#499).
  • Validated stored ids before building filesystem paths, quarantining unsafe ids (../poison) or non-numeric time.created; rejected NUL-byte paths in resolvePath (#499, #503).
  • Made the account store atomic + self-healing (checksummed WAL + temp-and-rename) and retried the transient-lock taxonomy (EBUSY/EPERM/ENOTEMPTY/EACCES/EAGAIN) across all writes (#499).
  • Persisted runtime-observability.json owner-only (0o600 / dir 0o700) on POSIX; removed Atomics.wait sleeps from the config-load and logger retry paths (#499, #503).

Quota & Forecast

Bugfixes

  • Detected an unsupported Codex model from the upstream error detail shape (not just the nested error envelope), surfacing a friendly "Codex unavailable" note across best/forecast/report/live-check instead of leaking raw upstream text (#501/#502).
  • Kept a genuine transient failure surfacing as the real error, not masked behind the friendly note (#501/#502).
  • Bounded prompt and release-metadata fetches by connect+body timeouts that cancel a stalled body (#499).

CLI

Improvements

  • status / list gained --json with a stable shape whether or not accounts are configured (#499).

Bugfixes

  • Required a strict integer for switch <index> (no silent float truncation) (#503).
  • Masked tokens/emails across log, debug-bundle, and status sinks; the debug bundle redacts the home prefix (Windows case/separator-insensitive), strips config credentials, and masks the account id (#499).

Dependencies

Bugfixes