Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,15 @@ For an opted-in, unpaused main account with a full usage window, the background

Background revalidation is separate and off by default. It requires Token Guardian, the `openai` provider's `proactive` refresh policy, and `tokenGuardian.codexWarmupEnabled`. It skips accounts awaiting deferred registration validation.

### After a ChatGPT plan upgrade

Refresh the main account's quota in Codex Auth after changing your ChatGPT plan. When the
authenticated usage response confirms a long window has replaced the five-hour window,
opencodex removes the old five-hour reading from both request admission and account selection.
An allowed response with primary and secondary windows can omit the optional tertiary window.
This recovery uses your included plan allowance and does not require enabling **Use credits**.
Partial or invalid usage data retains the previous block until recovery is confirmed.

### Cancelling main-account device reauthentication

When cancelling main-account device reauthentication, a temporary DELETE or network failure, or a response with an unknown or nonterminal status, keeps the active flow and the cancellation-failure indication so cancellation can be retried. Status polling normally continues so a login that completes can still be detected. If a retryable cancellation failure overlaps a non-2xx GET status response while the flow is `pending` or `committing`, either response order preserves cancellation retry on that same flow, with its last server-provided device code, verification URL, and phase. The GET HTTP failure still stops polling, but cancellation can be retried without starting a second login POST. A terminal `failed` response releases the flow and displays the normalized failure reason; only `succeeded` reports login success. A confirmed `cancelled` response releases the flow so a new device login can be started. A definitive HTTP 404 response with code `unknown_flow` also releases the expired flow ID so a new device login can be started, but does not report a successful login or confirmed cancellation. Late POST, GET, or DELETE responses from an earlier flow cannot change the new flow or report login success for it.
Expand Down
19 changes: 13 additions & 6 deletions src/codex/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,12 @@ export function setAccountQuotaFromParsed(
credentialGeneration: historyEvidence.writer.credentialGeneration, windows: historyWindows(historyEvidence.raw),
}, updatedAt);
}
// Legacy rotation keeps its existing carry behavior, but never inherits policy-only
// Rotation shares validated main-window retirement, but never inherits policy-only
// evidence that outlived its disk TTL. Policy has a separate, identity-checked base.
const next = mergeAccountQuota(quota, legacyExisting, updatedAt);
const displayQuota: MainPolicyQuotaObservation = isMain && mainWriter && policyQuota?.shortWindowAbsent === true
? { ...quota, shortWindowAbsent: true }
: quota;
const next = mergeAccountQuota(displayQuota, legacyExisting, updatedAt);
accountQuota.set(accountId, next);
if (isMain) {
if (mainWriter && policyQuota && snapshotHasUsage(policyQuota)) {
Expand Down Expand Up @@ -440,7 +443,7 @@ function mergeAccountQuota(
}
if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt;
if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds;
} else if (!policyEvidence || quota.shortWindowAbsent !== true) {
} else if (quota.shortWindowAbsent !== true) {
// Unknown usage is not a lower reading. Retain the entire known tuple: pairing
// its percentage with new metadata would silently extend or shorten its reset.
// An elapsed reset is the exception. It describes a window that has already rolled over,
Expand Down Expand Up @@ -879,22 +882,26 @@ function filterMainPolicyMonthlyQuota(
/**
* Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages.
* A measured long primary, or explicitly absent primary with measured weekly secondary,
* proves replacement only with complete long/null topology. Null supplies no usable evidence.
* proves replacement with long/null topology; an allowed two-window response may omit tertiary.
* Null supplies no usable evidence.
*/
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null {
const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null;
const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type));
const [primary, secondary, tertiary] = windows;
// WHAM explicitly reports absent windows as null; omissions cannot prove replacement.
// Primary and secondary must explicitly report their topology. WHAM also sends
// two-window responses without tertiary; an explicit allowed verdict disambiguates those
// from partial observations. A present tertiary still requires measured long/null evidence.
// Policy trusts one complete snapshot only when every non-null window is >=24h AND
// carries a valid usage reading: a long window without used_percent leaves that
// window's usage unknown, and unknown usage must never release a block.
// Headers never supply this proof, and reset time alone still cannot release a block.
if (quota && (isMeasuredLongWindow(primary)
|| (primary === null && isMeasuredLongWindow(secondary) && quota.weeklyPercent !== undefined))
&& (secondary === null || isMeasuredLongWindow(secondary))
&& (tertiary === null || isMeasuredLongWindow(tertiary))) {
&& (tertiary === null || isMeasuredLongWindow(tertiary)
|| (tertiary === undefined && data.rate_limit?.allowed === true))) {
return { ...quota, shortWindowAbsent: true };
}
return quota;
Expand Down
8 changes: 4 additions & 4 deletions structure/dashboard-and-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ in [GUI and management API](gui-and-management-api.md).
The dashboard is a local control surface, not a separate service. It should reflect the same config
and catalog invariants documented in this folder rather than inventing parallel state.

Codex quota cards consume the display cache from `src/codex/quota.ts`. A partial refresh
removes an omitted short tuple whose reset deadline has elapsed, so a stale model-derived
5h row does not persist on a weekly-only account. This is independent of the main-account
blocking hard-lock evidence and reset-notification history; their retention rules are documented in
Codex quota cards consume the display cache from `src/codex/quota.ts`. Partial refreshes remove
omitted short tuples after their reset deadline; validated main-window absence also removes
future-reset short tuples. Main-account hard-lock evidence and reset-notification history follow
separate retention rules in
[OpenAI account modes](providers/openai-tiers.md#quota-cache-and-short-window-history).
Codex account panels expose no Spark quota toggle or setting and retain quota refresh, pause/resume, selection order, reset-credit confirmation and Advanced settings; account/provider quota DTOs suppress retired windows upstream of the generic quota renderer, under [OpenAI quota scopes](providers/openai-tiers.md#public-provider-contract).

Expand Down
12 changes: 8 additions & 4 deletions structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,18 +414,22 @@ hard-lock tests registered in `scripts/test-layout/layout.json`, including

A single fresh valid WHAM response with a measured long primary, or an explicitly null primary and
measured secondary that supplies parsed weekly usage, can replace an obsolete short-window tuple.
Secondary and tertiary must each be explicitly null or explicitly long with a valid usage reading.
Secondary must be explicitly null or explicitly long with a valid usage reading. Tertiary follows
the same rule when present; a two-window response may omit it only with `rate_limit.allowed === true`.
Long means **at least 24 hours**, matching the parser's short/long discriminator; a one-day primary
qualifies, not only a seven-day or monthly window. The policy trusts that one reported topology;
it does not require repeated observations or independently confirm upstream window completeness.
Any omitted window field, an unreadable long window, an unknown duration, partial headers, or invalid usage
An omitted primary/secondary, omitted tertiary without that verdict, unreadable long window, unknown duration, partial headers, or invalid usage
cannot prove that the short window disappeared. All-null credits-only and tertiary-only Go/Free responses remain insufficient. Replacement proof belongs only to that observation and is never persisted;
the resulting weekly/monthly window still blocks at its long threshold (98% by default). This prevents old short-window exhaustion
from surviving indefinitely on a now weekly/monthly account. Coverage lives in
from surviving indefinitely on a now weekly/monthly account. A live main writer applies that same
retirement proof to the display/rotation cache, so account selection cannot keep refusing the
recovered main account on its old short tuple. Coverage lives in
`tests/codex-integration/main-quota-evidence-validation.test.ts`,
`tests/codex-integration/main-account-hard-lock-retirement.test.ts`,
`tests/codex-integration/main-quota-provenance.test.ts`, and
`tests/codex-integration/main-account-hard-lock-recovery.test.ts`.
`tests/codex-integration/main-account-hard-lock-recovery.test.ts`; request admission without credit
consent is covered by `tests/codex-integration/main-account-credit-renewal.test.ts`.

The policy reads a separately retained identity-tagged quota snapshot, so the legacy rotation
cache's six-hour expiry does not silently release a known block. A confirmed account transition
Expand Down
31 changes: 30 additions & 1 deletion tests/codex-integration/main-account-credit-renewal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { MAIN_CODEX_ACCOUNT_ID as MAIN } from "../../src/codex/account-id";
import { reconcileMainCodexAccountRuntimeState, resetMainCodexAccountIdentityTrackingForTests } from "../../src/codex/account-lifecycle";
import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "../../src/codex/account-runtime-state";
import { captureMainQuotaWriter, clearMainAccountInfoCache, getMainAccountInfoCache, isMainAccountIdentityGenerationLive, observeMainQuotaCredential, setMainAccountInfoCache } from "../../src/codex/main-account-cache";
import { clearAccountQuota, getMainPolicyQuota, setAccountQuotaFromParsed } from "../../src/codex/quota";
import { clearAccountQuota, getAccountQuota, getMainPolicyQuota, setAccountQuotaFromParsed } from "../../src/codex/quota";
import { hasSpendableCodexCredits, type CodexSpendableCredits } from "../../src/codex/quota-types";
import { resetQuotaQueryBackoffForTests } from "../../src/codex/quota-query-backoff";
import { clearCodexUpstreamHealth } from "../../src/codex/routing";
Expand Down Expand Up @@ -157,6 +157,35 @@ test("without renewal, five-minute credit expiry produces a local 429", async ()
expect(calls).toEqual([]);
});

test.each([[false, false], [true, false], [false, true], [true, true]] as const)(
"plan upgrade retires an exhausted short window without credit consent (hard lock=%j, tertiary null=%j)",
async (hardLock, tertiaryNull) => {
cfg.codexMainAccountHardLock = hardLock;
cfg.creditCodexAccountIds = [];
clearAccountQuota(MAIN);
setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000,
shortResetAt: Math.floor((now + HOUR) / 1000), weeklyPercent: 20 }, undefined,
captureMainQuotaWriter(accountId));
fetchWith(async () => Response.json({ plan_type: "prolite", rate_limit: {
allowed: true,
primary_window: { used_percent: 3, limit_window_seconds: 604_800,
reset_at: Math.floor((now + 7 * 24 * HOUR) / 1000) },
secondary_window: null,
...(tertiaryNull ? { tertiary_window: null } : {}),
}, credits: { has_credits: false, unlimited: false, balance: "0" } }));
const result = await fetchMainAccountInfoAttempt(true, 0);
expect(result.info.plan).toBe("prolite");
expect(result.info.quota?.weeklyPercent).toBe(3);
expect(getMainPolicyQuota()?.shortPercent).toBeUndefined();
await expect(resolveCodexAuthContext(new Headers(), cfg, "pool")).resolves.toMatchObject({ kind: "main-pool" });
for (const field of ["shortPercent", "shortObservedAt", "shortResetAt", "shortWindowSeconds"] as const) {
expect(getMainPolicyQuota()?.[field]).toBeUndefined();
expect(getAccountQuota(MAIN)?.[field]).toBeUndefined();
}
expect(calls).toEqual([whamUrl]);
},
);

test.each([false, true])("hidden-dashboard sweep renews at three minutes and admits past original expiry (unlimited=%j)", async unlimited => {
const original = now;
seed({ hasCredits: true, ...(unlimited ? { unlimited: true } : { balance: 5 }), observedAt: now });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { MAIN_CODEX_ACCOUNT_ID as MAIN } from "../../src/codex/account-id";
import { getMainAccountHardLockStatus } from "../../src/codex/main-account-hard-lock";
import { captureMainQuotaWriter, clearMainAccountInfoCache, observeMainQuotaIdentity } from "../../src/codex/main-account-cache";
import {
applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getMainPolicyQuota,
applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getAccountQuota, getMainPolicyQuota,
parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, type WhamUsageResponse,
} from "../../src/codex/quota";
import { removeTreeWithRetry } from "../helpers/remove-tree";
Expand Down Expand Up @@ -49,6 +49,26 @@ function publish(data: WhamUsageResponse): void {
}

describe("authoritative main 5h window retirement (#6244)", () => {
for (const primaryAbsent of [false, true]) {
test.each([0, 35, 98, 100])(`allowed two-window snapshot retires short usage in both caches (primary absent=${primaryAbsent}, weekly=%s)`, percent => {
setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000,
shortResetAt: Date.now() / 1000 + 3_600 }, undefined, captureMainQuotaWriter(account));
const measured = { ...weekly, used_percent: percent };
publish({ rate_limit: { allowed: true,
primary_window: primaryAbsent ? null : measured,
secondary_window: primaryAbsent ? measured : null,
} });
for (const quota of [getAccountQuota(MAIN), getMainPolicyQuota()]) {
expect(quota?.weeklyPercent).toBe(percent);
for (const field of ["shortPercent", "shortObservedAt", "shortResetAt", "shortWindowSeconds"] as const) {
expect(quota?.[field]).toBeUndefined();
}
expect(quota).not.toHaveProperty("shortWindowAbsent");
}
expect(getMainAccountHardLockStatus({}).state).toBe(percent < 98 ? "ready" : "blocked");
});
}

test.each([0, 35, 97.99, 98, 100])("explicit absent primary retires old 5h while weekly %s still governs", percent => {
publish({ rate_limit: {
primary_window: null, secondary_window: { ...weekly, used_percent: percent }, tertiary_window: null,
Expand All @@ -63,6 +83,17 @@ describe("authoritative main 5h window retirement (#6244)", () => {
});

test.each<WhamUsageResponse>([
{ rate_limit: { allowed: false, primary_window: weekly, secondary_window: null } },
{ rate_limit: { allowed: "true", primary_window: weekly, secondary_window: null } },
{ rate_limit: { allowed: true, primary_window: weekly } },
{ rate_limit: { allowed: true, secondary_window: weekly } },
{ rate_limit: { allowed: true, primary_window: {}, secondary_window: weekly } },
{ rate_limit: { allowed: true, primary_window: weekly, secondary_window: {} } },
{ rate_limit: { allowed: true, primary_window: weekly, secondary_window: null,
tertiary_window: { limit_window_seconds: 2_592_000 } } },
{ rate_limit: { allowed: true, primary_window: weekly, secondary_window: null,
tertiary_window: { used_percent: 0, limit_window_seconds: 18_000 } } },
{ rate_limit: { allowed: true, primary_window: { ...weekly, used_percent: 101 }, secondary_window: null } },
{ rate_limit: { secondary_window: weekly, tertiary_window: null } },
{ rate_limit: { primary_window: null, secondary_window: weekly } },
{ rate_limit: { primary_window: null, secondary_window: {}, tertiary_window: null } },
Expand Down Expand Up @@ -106,11 +137,12 @@ describe("authoritative main 5h window retirement (#6244)", () => {
expect(getMainAccountHardLockStatus({}, Date.now() + 30 * 24 * 60 * 60_000).state).toBe("blocked");
});

test("a stale identity writer cannot publish otherwise authoritative absence", () => {
test.each([false, true])("a stale identity writer cannot publish authoritative absence (tertiary null=%j)", tertiaryNull => {
const staleWriter = captureMainQuotaWriter(account)!;
clearMainAccountInfoCache();
observeMainQuotaIdentity(account);
const data = { rate_limit: { primary_window: null, secondary_window: weekly, tertiary_window: null } };
const data = { rate_limit: { allowed: true, primary_window: null, secondary_window: weekly,
...(tertiaryNull ? { tertiary_window: null } : {}) } };
setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, staleWriter, parseMainPolicyUsageQuota(data));
expect(getMainPolicyQuota()?.shortPercent).toBe(100);
expect(getMainAccountHardLockStatus({}).state).toBe("blocked");
Expand Down
6 changes: 4 additions & 2 deletions tests/codex-integration/main-quota-provenance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,11 @@ test("window replacement persists without carrying its proof into later partial
const publish = (data: WhamUsageResponse) => setAccountQuotaFromParsed(
MAIN, parseUsageQuota(data), undefined, writer, parseMainPolicyUsageQuota(data),
);
setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, shortResetAt: 1 }, undefined, writer);
setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000,
shortResetAt: Date.now() / 1000 + 3_600 }, undefined, writer);
expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked");
publish({ rate_limit: {
primary_window: null, secondary_window: { used_percent: 35, limit_window_seconds: 604_800 }, tertiary_window: null,
allowed: true, primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null,
} });
// Execute quota's actual debounced serializer through the existing deterministic clock.
const persisted = flushPersistence();
Expand All @@ -323,6 +324,7 @@ test("window replacement persists without carrying its proof into later partial
writeFileSync(join(testDir, "codex-quota-cache.json"), persisted);
expect(getMainAccountHardLockStatus(cfg).state).toBe("ready");
expect(getMainPolicyQuota()?.shortPercent).toBeUndefined();
expect(getAccountQuota(MAIN)?.shortPercent).toBeUndefined();
publish({ rate_limit: { primary_window: { used_percent: 99, limit_window_seconds: 18_000 } } });
publish({ rate_limit: { primary_window: { used_percent: 0 } } });
expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked");
Expand Down
Loading