Releases: ndycode/codex-multi-auth
Releases · ndycode/codex-multi-auth
v2.3.0-beta.0
Runtime Rotation
Features
- Sequential / drain-first account scheduling (opt-in). A new
schedulingStrategysetting (CODEX_AUTH_SCHEDULING_STRATEGY/
schedulingStrategy, defaulthybrid) adds asequentialmode that drains one
account fully before moving to the next, instead of spreading load across the
pool. Set it tosequentialto 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.
hybridremains the default and keeps the
existing weighted health/token/freshness selection plus per-session affinity.
sequentialis 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.
Insequentialmode 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
betadist-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
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 inaccountSkipReasonsand keeps the forecast reporting a working account as unavailable. - Added
recordRuntimeAccountRecovery(index)on the proxy success path: it removes the account's entry fromaccountSkipReasonsandlastPoolExhaustionSkipReasons, is a no-op when nothing is recorded, and ignores non-integer or negative indices.
Quota & Forecast
Bugfixes
- Stopped
forecast --livemarking 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-limitedwhengetRateLimitResetTimeForFamilyfinds no active reset (including model-scoped keys likecodex:5h), andcooling-down:...whencoolingDownUntilis 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'sforecast-runtime-alignmentcheck, which shares the forecast evaluation, so the same stale state no longer raises a spurious warning.
v2.2.1
Launcher
Bugfixes
- Rewrote
mcodexas a Node bin (scripts/mcodex.js); the old bash script shipped as a Windows bin died withHCS_E_SERVICE_NOT_AVAILABLEwhen a WSL stub shadowed git-bash. Zero bash dependency on the default path;tmux/watchinvoked 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/SIGINTto the spawned child so it is never orphaned.
Auth
Bugfixes
- Isolated OAuth concurrent-login state in per-call closures instead of the shared
http.Serverinstance, so parallel logins can't cross-bind callbackcode/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; debouncedlastUsedAtwrites 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/responsesrequest into the codex family (CURRENT_CODEX_MODEL) instead of the generalgpt-5.5family, keeping rotation/cooldown/budget accounting correct. - Stripped inbound
cookie/proxy-authorizationon 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/affinityGenerationthrough 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
statand mtime compare-and-swap. - Created secret directories
0o700; floored fractional indices and coercedNaNinclampIndex.
Quota & Forecast
Improvements
gpt-5.5is now the default live/quota probe model, legacy fallback chain (gpt-5.4→gpt-5.3-codex→gpt-5.2-codex→gpt-5-codex) preserved in one sharedQUOTA_PROBE_MODEL_CHAIN.- Codex-unavailable accounts are labeled "signed in" not "working"; live check gained
Codex available/signed in only/need re-logincounters (!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
--modelparsing 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/switchindices instead of truncating; rejected a flag-likebudget checkkey. - Shipped
.codex-plugin/plugin.jsonin 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
Launcher
Improvements
- New
mcodexlauncher (#500): launches Codex with a cached status line (model, reasoning effort, cwd, active account, quota usage, plan, cache age) printed before startup.mcodex --tmuxruns inside tmux with mouse scrollback;--tmux --live-accountsadds a livecodex-multi-auth listpane;--monitoris 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 whenperProjectAccountsis on and Codex CLI sync is off. Toggle withCODEX_MULTI_AUTH_STATUSLINE=0.
Hardening
- Validated
MCODEX_MONITOR_INTERVAL/MCODEX_TMUX_HISTORY_LIMITas numeric before interpolating intowatch/tmux commands (no shell injection). --monitor/--live-accountsfail fast whenwatchis 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-authorizationon 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-numerictime.created; rejected NUL-byte paths inresolvePath(#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.jsonowner-only (0o600/ dir0o700) on POSIX; removedAtomics.waitsleeps from the config-load and logger retry paths (#499, #503).
Quota & Forecast
Bugfixes
- Detected an unsupported Codex model from the upstream error
detailshape (not just the nestederrorenvelope), surfacing a friendly "Codex unavailable" note acrossbest/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/listgained--jsonwith 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
- Bumped
vitestto^4.1.8(dev-only) to clear GHSA-5xrq-8626-4rwp (#503).
v2.1.13-beta.1
Accounts
Fixes
- Account
workspacesandcurrentWorkspaceIndexnow survive a load
round-trip. The strict V3 storage schema (AccountMetadataV3Schema) did not
declare these fields, so Zod stripped them on every read; login captured the
workspaces and wrote them to disk, but the next load wiped them. This is the
root cause of workspace labels appearing empty after login and of two
same-email accounts being indistinguishable. formatAccountLabelsurfaces the active workspace name, so same-email accounts
in different workspaces stay distinguishable, for example
Account 1 ([Personal Plus], user@gmail.com, id:g-AAAA). All prior label
formats are preserved when no workspace is tracked.
Features
status/listnow list every workspace beneath an account that tracks more
than one, marking the active workspace and flagging disabled ones.- New
workspace <account> [workspace]command: with only an account index it
lists that account's workspaces; with a workspace index it sets the active
workspace and persists it. login --org <org_id>(and--org=<id>) binds a login to a specific
workspace/org. It reuses theCODEX_AUTH_ACCOUNT_IDoverride that every login
resolver already honors, scoped to the invocation and restored afterward, so a
second workspace can be registered on demand instead of always resolving to the
default org.
Release Hygiene
Tests
- Schema round-trip preservation and a workspace-load regression that locks the
fix in place. - Label disambiguation coverage and
formatWorkspaceLinesoutput coverage
(active marker, disabled annotation, indentation). - Nine
workspace-command cases (list, switch, persistence, already-active
no-op, disabled rejection, out-of-range and non-numeric indices). login --orgargument parsing and missing-value handling.
Notes
- The issue #486 503 root cause (doctor reports all green, runtime still returns 503) is not fixed by this prerelease.
- Whether a real two-workspace login returns both orgs in one token or one at a time is unconfirmed. Both paths are supported (the
workspacecommand for the former,login --orgfor the latter); a sanitizedaccounts.jsonfrom a real two-workspace account would confirm which path users hit.
v2.1.12
Runtime Rotation
Fixes
- Mirrored trusted
[hooks.state.*]TOML blocks from the originalconfig.tomlinto runtime shadow configs when the hook key points at the user'shooks.json. - Preserved CRLF line endings and full hook-state table bodies while rewriting hook keys to the shadow
hooks.jsonpath. - Kept unrelated hook-state entries, alternate hook files, and existing shadow entries unchanged.
Release Hygiene
Tests
- Added regression coverage for literal TOML keys, CRLF preservation, table-like content inside strings and arrays, alternate hook paths, long hook keys, shadow
hooks.jsonvisibility, and concurrent runtime shadow launches. - Linked duplicated TOML block scanner helpers in production and test code to reduce future drift.
v2.1.10
Runtime Rotation
Bugfixes
- Runtime pool exhaustion on
no-accountnow resets volatile runtime trackers, reloads accounts from disk once, and retries selection even when stale in-memory circuit breakers are open. This prevents a long-lived proxy from staying wedged after disk state has recovered. - Runtime proxy shutdown now flushes only the active account manager after a stale-state reload, avoiding stale-vs-fresh manager save races during close.
- Pinned-account hard-fail behavior remains preserved: unavailable pinned accounts still return the explicit pinned-account error instead of silently rotating.
Diagnostics
Improvements
- Pool-exhausted 503 responses include per-account runtime skip reasons and a targeted recovery hint.
reportanddoctornow include persisted quota-cache inputs when computing forecast readiness, so quota-exhausted cache state cannot be reported as ready by those diagnostics.doctorkeeps theforecast-runtime-alignmentwarning for disk/runtime divergence.rotation reset-runtime --jsonno longer clears runtime diagnostics if the app bind restart fails, preserving evidence needed for follow-up diagnosis.
Release Hygiene
Improvements
- The npm package now includes only runtime/install scripts instead of internal benchmark, audit, and maintenance scripts.
- Production dependency patch lines were refreshed (
undiciandzod) while keeping the published package on the current Node 18-compatible runtime line. - Release index labels were corrected so older release-note rows are not shown as current stable.
v2.1.9
CLI
Bugfixes
codex-multi-auth login --device-authno longer exits withDetected unsettled top-level awaitbefore the user can complete browser authorization. The polling sleep timer was being unref'd, which is correct for library/background callers but caused Node to exit during the foreground CLI flow. The CLI call site now opts in to a newkeepAliveoption onrunDeviceAuthFlowso the event loop stays alive until polling resolves or the abort signal fires.codex-multi-auth login --device-auth,--manual, and--no-browsernow run the requested transport even when the account pool already has entries. Previously the Accounts Dashboard menu interceptedloginwhenever any account was present, silently ignoring the explicit flag.- Declining
Add another account?after a successful explicit-mode add now exits cleanly. Previously the outerloginFlowloop would re-enter the same transport because the dashboard had been bypassed, trapping the user in a fresh sign-in prompt. - Cancelling an explicit-mode sign-in (for example pressing Ctrl-C during browser/manual auth) now exits cleanly instead of falling back to a fresh transport invocation.
- Reaching
ACCOUNT_LIMITS.MAX_ACCOUNTS(20) during an explicit-mode add now exits cleanly after printing the cap message. Previously the outer loop would silently start another sign-in session despite the cap.
Public API
Improvements
DeviceAuthFlowOptionsadds an optionalkeepAlive?: booleanfield. Library and background consumers retain the previous behavior (sleep timers are unref'd so polling does not hold the event loop open). Foreground CLI callers can passkeepAlive: trueto keep the loop alive while polling for completion. Abort handling stays correct in both modes:clearTimeoutruns in the abort listener so the timer ref is always released on cancellation.
v2.1.8
CLI
Improvements
- Added
codex-multi-auth unpinto clear the manual pin set byswitch, bumpaffinityGenerationso the proxy invalidates session affinity on the next request, and resume hybrid rotation. codex-multi-auth statusnow surfaces the pinned account index and warns when the runtime is using a different account than the pin requests.codex-multi-auth switch <index>now writespinnedAccountIndexthat the proxy honors before session affinity, hybrid scoring, and pool fallback;bestclears the pin.
Bugfixes
statusno longer printsPinned: account NaNfor corrupt pins; the guard tightens toNumber.isInteger, shows the raw stored value, and points atunpin.
Runtime Rotation Proxy
Bugfixes
- Runtime rotation proxy now honors manual switch on the Codex Desktop app via a content-hash-keyed per-path read of the storage file; the pinned path is exempt from
markSwitched/saveToDiskDebounced/syncCodexCliActiveSelectionForIndex. - Session affinity no longer locks the desktop app to one account for 20 minutes;
affinityGenerationbumps fromswitch/unpin/besttriggerSessionAffinityStore.clearAll()beforechooseAccount, including for the in-flight request. - When the pinned account is unavailable, the proxy now returns HTTP 503 with
error.code = codex_pinned_account_unavailableinstead of silently rotating. AccountManager.buildStorageSnapshotno longer wipespinnedAccountIndex/affinityGenerationon routine debounced saves; it refreshes from disk before serializing.- Concurrent
affinityGenerationincrements across CLI processes are now atomic viaMath.max(inMemory, disk) + 1. unpinnow usessaveAccountsWithRetryso a transient Windows file lock cannot silently lose the user's intent.- Replaced a synchronous busy-wait in
readStorageMetaFromDiskwith a single read plus per-path content-hash cache fallback that preserves the last snapshot on transient errors. - Runtime rotation proxy storage cache is now a
Map<string, StorageMetaSnapshot>keyed by absolute path, fixing cross-instance and cross-worker pin leakage.
2.1.7
Install / Uninstall
Improvements
- Added
codex-multi-auth uninstall [--dry-run] [--json] [--clear-accounts]that reverses postinstall: unbinds Codex app rotation, removes OS launchers, strips the plugin entry fromCodex.json, clears thenode_modulescache, and conservatively deletes sharedbun.lockonly when no other Codex plugins remain.
Bugfixes
- Removed the dead
preuninstallentry frompackage.jsonbecausenpm@7+no longer runs it; to uninstall fully, runcodex-multi-auth uninstallthennpm uninstall -g codex-multi-auth.scripts/preuninstall.jsis preserved and still callable vianode scripts/preuninstall.js. - Dry-run
uninstallnow computes thebun.lockdecision from the actualCodex.jsonplugin list. --clear-accountswarns and exits non-zero when no handler is wired.- Dry-run uninstall preview no longer requires
scripts/codex-app-launcher.json disk. removePluginFromListpre-filters falsy entries withlist.filter(Boolean).
Core
Bugfixes
hydrateRuntimeEmailsno longer collapses accounts sharingaccountId === undefinedinto the sameMapentry.restoreTopLevelModelProvider/restoreTopLevelResponseStoragenow splice missing keys ahead of the first section header instead of producing invalid TOML.runFix,runHealthCheck, and forecastsaveQuotaCachepaths now downgrade transientEBUSY/EPERMto a partial-success warning viaquotaCacheSaveError.withStorageLocknow initializesreleaseLockas a typed no-op.
Runtime Rotation Proxy
Bugfixes
- App-bind now requires
router.state === "running"and a live PID before reusingexistingState, so a dead router cannot trigger aconfig.tomlrewrite. - Stream-failover read promise is now hoisted before the soft/hard timeout split.