Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f9e7b93
fix(codex): renew opted-in pool credit evidence on the existing sweep
Ingwannu Oct 19, 2026
af500bf
test(codex): assert renewal performs no native-main access
Ingwannu Oct 19, 2026
b0d8fba
fix(codex): limit Direct hard-lock fallback to the locked account
luvs01 Oct 18, 2026
006b9d2
fix(responses): break hard-lock caller-check import cycle
luvs01 Oct 18, 2026
c77f226
fix(codex): revalidate hard-lock fallback targets before request rewr…
luvs01 Oct 19, 2026
943cf76
fix(codex): clear failure windows when account identities are removed
luvs01 Oct 18, 2026
29ea3e3
fix(codex): fence late failure-window samples by dispatch identity
luvs01 Oct 18, 2026
07dc642
fix(codex): revalidate tagged failure samples after credential replac…
luvs01 Oct 18, 2026
19d8843
fix(codex): preserve failure windows when credential snapshots fail
luvs01 Oct 18, 2026
404006d
fix(codex): reject non-object credential snapshot shapes
luvs01 Oct 18, 2026
9d5c2b0
refactor(responses): keep passthrough-dispatch within its line budget
lidge-jun Oct 19, 2026
b5de9ca
fix(codex): keep a paused main caller out of Pool fallback
lidge-jun Oct 19, 2026
177a967
fix(codex): recheck a predicted-reset main hard lock with metadata (#…
lidge-jun Oct 19, 2026
5804071
test(codex): await the async hard-lock fallback override in #7340 cases
lidge-jun Oct 19, 2026
3f4479d
fix(codex): recheck Direct hard-lock ownership synchronously before r…
lidge-jun Oct 19, 2026
7f35db5
fix(codex): keep predicted-reset hold rechecks paced across transitio…
lidge-jun Oct 19, 2026
6a3657a
fix(codex): honor a revised reset that elapses between hold sweeps (#…
lidge-jun Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -470,9 +470,10 @@ reset changes and expires after six hours or at the observed reset, whichever co

An unreadable 5h reading cannot hide a weekly block. Unknown usage does not fabricate a zero, and a missing reading does
not erase an already measured blocking tuple. A predicted reset time alone does not unlock it.
While blocked, the minute sweep waits for the latest known blocking reset, then checks owned usage.
If no future reset is known or a check remains blocked, recovery uses a capped 5/10/20/40/60-minute
schedule; a longer `Retry-After` also delays profile and token preparation. Only fresh valid usage or authoritative window-absence evidence
While blocked, recovery checks owned usage metadata on a capped 5/10/20/40/60-minute schedule, so a reset
redeemed outside opencodex is noticed before the predicted reset; the first check waits five minutes, checks
stay at least five minutes apart, and the next check is pulled in to the latest known blocking reset. A paused
Comment on lines +474 to +475

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the transient-auth retry exception

When a predicted-reset WHAM read returns a nonterminal 401/403, runMainAccountHardLockRecovery clears mainHardLockRecoveryAttempt, so the next one-minute sweep retries immediately; the added regression test explicitly expects reads at minutes 5 and 6. Therefore the public claim that checks “stay at least five minutes apart” is false for this supported failure path and overstates the probe-rate bound. Either preserve the five-minute floor for transient-auth retries or document this exception.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

or reauthentication-required main receives no checks before its predicted reset. A longer `Retry-After` also delays profile and token preparation. Only fresh valid usage or authoritative window-absence evidence
can lift the block. In Pool mode, a quota `--refresh` bypasses cache freshness but still honors failed-read pacing;
a deferred read makes no new diagnostic attempt. Other pause, reauthentication, and upstream limits remain independent.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR
| `cacheRetention?` | `"none" \| "short" \| "long"` | first-party Anthropic: `"long"`; other Anthropic-compatible endpoints: `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. When unset, requests to `api.anthropic.com` (API key or the OAuth account pools) use the 1-hour TTL, because agentic turns often pause longer than five minutes and a 1-hour write (2x base input instead of 1.25x) pays for itself on the first reuse; Anthropic-compatible gateways and vendor endpoints keep the 5-minute TTL because their 1-hour support is undocumented. On native `/v1/messages` passthrough, caller breakpoints without a `ttl` get `"1h"` unless the request pins any breakpoint to `"5m"`, in which case it is forwarded unchanged. On a translated `/v1/messages` route, caller breakpoints that all ask for `ttl: "1h"` mean `"long"` and any explicit `"5m"` means `"short"`. An explicit value always wins. Changing the TTL makes each open conversation write its cache once more, since 5-minute and 1-hour entries are separate. Cost estimates price writes reported as 1-hour at the 1-hour rate. |
| `tokenGuardian?` | `OcxTokenGuardianConfig` | off | Optional proactive OAuth refresh and Codex-account warmup policy. |

Opted-in pool accounts with a full included usage window refresh their credit evidence through the existing background sweep from three minutes of age. Ordinary response headers do not refresh credit balances. A failed metadata read never extends the five-minute spending-evidence lifetime.


Across these strategies, each account's effective threshold is its `codexAccountAutoSwitchThresholds` entry when present, otherwise global `autoSwitchThreshold`. A zero override disables only proactive usage switching; startup binding, hard-lock, cooldown, entitlement checks, and failure recovery remain enforced.

Setting an account's switching threshold to `0` does not disable main-account hard-lock, startup
Expand Down
11 changes: 8 additions & 3 deletions src/codex/account-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
import { clearAccountQuota } from "./quota";
import { forgetManualPreference } from "./routing/active-account";
import { deleteAllHealthForAccount } from "./routing/health-store";
import { forgetCodexFailureWindow } from "./routing/failure-window";
import { clearThreadAccountMapForAccount } from "./routing/thread-affinity";

import { clearCodexPoolRefreshFailure } from "./pool-refresh-backoff";
Expand Down Expand Up @@ -49,11 +50,15 @@ export function purgeCodexAccountRuntimeState(accountId: string): void {
clearAccountNeedsReauth(accountId);
clearAccountQuota(accountId);
clearThreadAccountMapForAccount(accountId);
// Deliberately not via `./routing`: the account-delete path only needs the health rows and
// the manual preference gone, and importing the router here closed a cycle through
// routing/health-store and routing/active-account.
// Deliberately not via `./routing`: the account-delete path only needs the health rows
// and their sibling routing state gone, and importing the router barrel here closed a
// cycle through routing/health-store and routing/active-account. The leaf imports below
// carry every per-account forget that barrel helper used to perform — including the
// failure window, whose degrade and pin-warning state must not leak into a replacement
// account or a new physical login behind __main__.
deleteAllHealthForAccount(accountId);
forgetManualPreference(accountId);
forgetCodexFailureWindow(accountId);
clearCodexPoolRefreshFailure(accountId);
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
clearMainAccountInfoCache();
Expand Down
35 changes: 27 additions & 8 deletions src/codex/account-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,25 +136,37 @@ export function peekCodexAccountRecordStore(): Record<string, CodexAccountCreden

/** Read the Codex account record store from disk with hardened permissions, and hand it to the opt-in decision-quota slot (a no-op unless JEV quota signals are active). */
function loadCodexAccountRecordStore(): CodexAccountStore {
const result = loadCodexAccountRecordStoreResult();
return result.status === "ok" ? result.records : {};
}

type CodexAccountRecordStoreResult = { status: "ok"; records: CodexAccountStore } | { status: "failed" };

/** Retain read/parse failure separately for consumers that must preserve existing evidence. */
function loadCodexAccountRecordStoreResult(): CodexAccountRecordStoreResult {
const path = codexAccountsPath();
hardenConfigDir();
hardenExistingSecret(path);
if (!existsSync(path)) { notifyCodexStoreLoaded({}); return {}; }
const normalized: CodexAccountStore = {};
try {
const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
if (!isObject(raw)) throw new Error("invalid Codex account store shape");
for (const [id, value] of Object.entries(raw)) {
const record = normalizeRecord(value);
if (record) normalized[id] = record;
}
} catch {
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
notifyCodexStoreLoaded({});
return { status: "ok", records: {} };
}
backupInvalidConfig(path);
notifyCodexStoreLoaded({});
return {};
return { status: "failed" };
}
// Outside the parse try: roster publication is advisory and must never route a loaded store into the invalid-config backup path.
notifyCodexStoreLoaded(normalized);
return normalized;
return { status: "ok", records: normalized };
}

/** Atomically write the Codex account store, then hand it to the opt-in decision-quota slot so active advisory evidence tracks the persisted credentials. */
Expand Down Expand Up @@ -400,6 +412,10 @@ export function isCodexAccountGenerationLive(id: string, generation: number): bo
return !!record?.credential && record.deletedAt == null && record.generation === generation;
}

/** Callable for existing liveness consumers; stateful consumers can distinguish unknown snapshots. */
export type CodexAccountGenerationLiveCheck = ((id: string, generation: number) => boolean)
& { readonly snapshotStatus: "ok" | "failed" };

/**
* The same verdict as {@link isCodexAccountGenerationLive}, over ONE store load.
*
Expand All @@ -409,12 +425,15 @@ export function isCodexAccountGenerationLive(id: string, generation: number): bo
* several accounts in one synchronous pass on the request path, so it opens one checker and
* closes over the snapshot instead.
*/
export function beginCodexAccountGenerationLiveCheck(): (id: string, generation: number) => boolean {
const snapshot = loadCodexAccountRecordSnapshot();
return (id, generation) => {
export function beginCodexAccountGenerationLiveCheck(): CodexAccountGenerationLiveCheck {
let result: CodexAccountRecordStoreResult;
try { result = loadCodexAccountRecordStoreResult(); }
catch { result = { status: "failed" }; }
const snapshot = result.status === "ok" ? result.records : {};
return Object.assign((id: string, generation: number) => {
const record = snapshot[id];
return !!record?.credential && record.deletedAt == null && record.generation === generation;
};
}, { snapshotStatus: result.status });
}

export function saveCodexAccountCredentialIfGeneration(
Expand Down
Loading
Loading