From abff5228dcc22a78eba29ee928356a866088c315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Wed, 23 Sep 2026 09:14:50 +0900 Subject: [PATCH 1/5] fix(codex): retire stale short-window main-account hard locks --- .../ko/reference/cli/providers-accounts.md | 4 + .../docs/reference/cli/providers-accounts.md | 4 + src/codex/quota.ts | 27 ++++- structure/providers/openai-tiers.md | 15 ++- .../main-account-hard-lock-recovery.test.ts | 12 ++ .../main-quota-evidence-validation.test.ts | 109 +++++++++++++++++- 6 files changed, 161 insertions(+), 10 deletions(-) diff --git a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md index bc618edfbea..956d6713574 100644 --- a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md @@ -88,6 +88,10 @@ ocx login anthropic 실제 사용량을 다시 확인하며, 조회 실패나 잘못된 수치는 차단을 풀지 않습니다. 일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다. +새 사용량 응답에서 현재 계정에 주간·월간 창만 존재한다고 명확히 확인되면, 오래된 5h 수치는 +제거하고 현재 창에 동일한 99% 기준을 적용합니다. 창 정보가 누락되거나 응답 헤더만 일부 +도착한 경우에는 이전 차단을 해제하지 않습니다. + 저장되는 옵션은 OpenCodex의 `config.json`에 있는 `"codexMainAccountHardLock": true`이며, 기본값은 꺼짐입니다. 식별된 메인 계정의 새 요청을 막는 기능이지 마지막 1%를 예약하는 기능은 아닙니다. 진행 중 요청, 식별되지 않은 키링 계정, 프록시 밖 요청은 사용량을 더 쓸 수 있습니다. diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 11d568ab92f..51e705b6ad0 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -151,6 +151,10 @@ not erase an already measured blocking tuple. A predicted reset time alone does While blocked, the existing once-per-minute background cycle checks fresh owned usage; failed or invalid readings retain the block. Other pause, reauthentication, and upstream limits remain independent. +If a fresh usage response explicitly confirms that the account now has only weekly/monthly windows, +protection discards the obsolete 5h reading and evaluates the current window at the same 99% threshold. +Missing window metadata or partial response headers alone cannot clear a previous block. + The persisted option is `"codexMainAccountHardLock": true` in OpenCodex's `config.json`; it is off by default. This protects new requests using the identified main account, not the last 1% itself: already-running requests, unmatched caller-owned keyring credentials, and traffic outside the diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 56ba7abdd36..37d3f9d4250 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -28,6 +28,8 @@ type QuotaDiskFile = { }; type MainPolicyQuota = { identityKey: string; quota: StoredAccountQuota }; +/** Fresh WHAM topology proof is consumed by the merge, never retained in a cache or DTO. */ +type MainPolicyQuotaObservation = Omit & { shortWindowAbsent?: true }; let mainPolicyQuota: MainPolicyQuota | null = null; let diskHydrated = false; let persistTimer: ReturnType | null = null; @@ -198,6 +200,11 @@ function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): bo && seconds >= MONTHLY_WINDOW_MIN_SECONDS; } +function isExplicitLongWindow(window: WhamUsageWindow | null | undefined): boolean { + const seconds = window?.limit_window_seconds; + return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= WEEKLY_WINDOW_MIN_SECONDS; +} + function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean { const minutes = windowMinutes_(windowMinutes); return minutes !== undefined && minutes >= MONTHLY_WINDOW_MIN_MINUTES; @@ -271,7 +278,7 @@ export function setAccountQuotaFromParsed( quota: Omit | null, writerGeneration = captureConfigGeneration(), mainWriter?: MainQuotaWriter, - policyQuota: Omit | null = quota, + policyQuota: MainPolicyQuotaObservation | null = quota, historyEvidence?: QuotaObservationEvidence, ): void { quota = withoutRetiredCodexQuota(quota); @@ -314,7 +321,7 @@ export function setAccountQuotaFromParsed( /** One partial-window merge contract for legacy quota and identity-bound policy evidence. */ function mergeAccountQuota( - quota: Omit, + quota: MainPolicyQuotaObservation, existing: StoredAccountQuota | undefined, updatedAt: number, policyEvidence = false, @@ -376,7 +383,7 @@ function mergeAccountQuota( } if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt; if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds; - } else { + } else if (!policyEvidence || 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, @@ -792,10 +799,20 @@ function filterMainPolicyMonthlyQuota( } /** Ordinary main policy rejects an entire message containing any invalid numeric window. */ -export function parseMainPolicyUsageQuota(data: WhamUsageResponse): Omit | null { +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; - return filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type)); + const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type)); + const [primary, secondary, tertiary] = windows; + // A complete, valid long-window WHAM response can retire an old 5h policy tuple. + // The primary must declare its duration; absent secondary windows may be null or omitted. + // Headers never supply this proof, and reset time alone still cannot release a block. + if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary) + && (secondary == null || isExplicitLongWindow(secondary)) + && (tertiary == null || isExplicitLongWindow(tertiary))) { + return { ...quota, shortWindowAbsent: true }; + } + return quota; } export function parseUsageQuota(data: WhamUsageResponse): Omit | null { diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index e6f650c78d3..9810c976bda 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -274,10 +274,10 @@ This stops partial weekly/Spark or credits-only refreshes from renewing obsolete 5h rows through the cache-wide `updatedAt` timestamp. Plan labels do not suppress real windows. The separately retained main-policy snapshot preserves omitted blocking short evidence even after -its reset clock passes. Credits-only, weekly-only, and metadata-only updates cannot remove an +its reset clock passes. Credits-only, partial weekly-only, and metadata-only updates cannot remove an existing blocking short usage reading or release its hard lock; a fresh short reading can replace -it. Expired non-blocking short evidence is dropped, so it cannot take priority over a fresh blocking -weekly reading. +it. A validated long-primary WHAM snapshot can also retire the short tuple as described below. +Expired non-blocking short evidence is dropped, so it cannot take priority over a fresh blocking weekly reading. The Codex writer explicitly asks `src/quota/reset-observer.ts` to retain an absent short window in `src/quota/reset-seen-store.ts`, with its original observation time. Detection compares only @@ -301,6 +301,15 @@ release the block. Policy validation precedes legacy clamping. Supplementary mon become the fallback governing window without a monthly-only plan or explicit primary-monthly evidence. Previously unobserved usage is unknown, not fabricated headroom. +A fresh valid WHAM response with an explicitly long primary window can replace an obsolete +short-window tuple when secondary and tertiary windows are absent or also explicitly long. +An unknown primary duration, partial headers, or invalid usage cannot prove that the +short window disappeared. Replacement proof belongs only to that observation and is never persisted; +the resulting weekly/monthly window still blocks at 99%. This prevents old short-window exhaustion +from surviving indefinitely on a now weekly/monthly account. Coverage lives in +`tests/codex-integration/main-quota-evidence-validation.test.ts` and +`tests/codex-integration/main-account-hard-lock-recovery.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 invalidates old evidence. Request-owned bearers are matched only against a credential and effective diff --git a/tests/codex-integration/main-account-hard-lock-recovery.test.ts b/tests/codex-integration/main-account-hard-lock-recovery.test.ts index f0a803ef764..d072ab7f898 100644 --- a/tests/codex-integration/main-account-hard-lock-recovery.test.ts +++ b/tests/codex-integration/main-account-hard-lock-recovery.test.ts @@ -123,6 +123,18 @@ afterEach(async () => { }); describe("main hard-lock background recovery", () => { + test("owned metadata recovery replaces an obsolete short block with the current weekly window", async () => { + const calls = fetchWith(async () => Response.json({ plan_type: "pro", rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, + } })); + await runMainAccountHardLockRecovery(config()); + expect(calls).toEqual([whamUrl]); + expect(getMainAccountHardLockStatus(config())).toEqual({ enabled: true, state: "ready" }); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getMainPolicyQuota()?.weeklyPercent).toBe(35); + expect(getNativeMainProfileRequestCount()).toBe(0); + }); + test("existing sweep hook forces fresh WHAM past cache/reset without adding a timer", async () => { let percent = 99; const calls = fetchWith(async () => usage(percent)); diff --git a/tests/codex-integration/main-quota-evidence-validation.test.ts b/tests/codex-integration/main-quota-evidence-validation.test.ts index 1ed42205fed..357a875af25 100644 --- a/tests/codex-integration/main-quota-evidence-validation.test.ts +++ b/tests/codex-integration/main-quota-evidence-validation.test.ts @@ -1,12 +1,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; 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 { - clearAccountQuota, getAccountQuota, getMainPolicyQuota, parseMainPolicyUsageQuota, + applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getAccountQuota, getMainPolicyQuota, parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, updateAccountQuota, type WhamUsageResponse, } from "../../src/codex/quota"; import { removeTreeWithRetry } from "../helpers/remove-tree"; @@ -144,6 +144,111 @@ describe("raw policy evidence validation", () => { }); }); +describe("main policy window replacement", () => { + const cfg = { codexMainAccountHardLock: true }; + const weeklySeconds = 7 * 24 * 60 * 60; + const monthlySeconds = 30 * 24 * 60 * 60; + + function publish(data: WhamUsageResponse) { + setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, writerFor(), parseMainPolicyUsageQuota(data)); + } + + function retainedShort() { + const old = Date.now() - 16 * 24 * 60 * 60_000; + writeColdPolicy({ shortPercent: 100, shortWindowSeconds: 18_000, + shortObservedAt: old, shortResetAt: old / 1000 + 300, weeklyPercent: 35 }); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + } + + for (const [field, seconds] of [["weeklyPercent", weeklySeconds], ["monthlyPercent", monthlySeconds]] as const) { + test.each([0, 35, 98.99, 99, 100])(`fresh ${field}=%s replaces a retired persisted short window`, percent => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: percent, limit_window_seconds: seconds }, secondary_window: null, + } }); + const policy = getMainPolicyQuota(); + expect(policy?.[field]).toBe(percent); + for (const key of ["shortPercent", "shortResetAt", "shortObservedAt", "shortWindowSeconds"] as const) { + expect(policy?.[key]).toBeUndefined(); + } + // Replacement proof is per-observation, never a persisted permission to drop future evidence. + expect(policy).not.toHaveProperty("shortWindowAbsent"); + expect(getMainAccountHardLockStatus(cfg).state).toBe(percent < 99 ? "ready" : "blocked"); + publish({ rate_limit: { primary_window: { used_percent: 99, limit_window_seconds: 18_000 } } }); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + } + + test.each([ + { primary_window: { used_percent: 35 } }, + { primary_window: { limit_window_seconds: weeklySeconds }, secondary_window: null }, + { primary_window: { used_percent: -1, limit_window_seconds: weeklySeconds }, secondary_window: null }, + { primary_window: { used_percent: 101, limit_window_seconds: weeklySeconds }, secondary_window: null }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: {} }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, + secondary_window: { used_percent: 99, limit_window_seconds: 18_000 } }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + tertiary_window: { used_percent: 99, limit_window_seconds: 18_000 } }, + ])("partial, invalid or short-bearing metadata retains the old block: %j", rate_limit => { + retainedShort(); + publish({ rate_limit }); + expect(getMainPolicyQuota()?.shortPercent).toBe(100); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("a declared long secondary cannot hide the current weekly limit", () => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: monthlySeconds }, + secondary_window: { used_percent: 99, limit_window_seconds: weeklySeconds }, + } }); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("a valid long primary also proves replacement when absent secondary is omitted", () => { + retainedShort(); + publish({ rate_limit: { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds } } }); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + }); + + test("long-window response headers alone do not retire a known short block", () => { + retainedShort(); + applyAccountQuotaFromUpstreamHeaders(MAIN, new Headers({ + "x-codex-primary-used-percent": "35", "x-codex-primary-window-minutes": "10080", + }), undefined, writerFor()); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("window replacement persists without carrying its proof into later partial updates", async () => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + } }); + await Bun.sleep(350); + const path = join(home, "codex-quota-cache.json"); + const persisted = readFileSync(path, "utf8"); + clearAccountQuota(); + writeFileSync(path, persisted); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + expect(getMainPolicyQuota()).not.toHaveProperty("shortWindowAbsent"); + 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"); + }); + + test("a superseded identity cannot retire the current account's short block", () => { + const staleWriter = writerFor("fixture-main-b"); + retainedShort(); + const data = { rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + } }; + setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, staleWriter, parseMainPolicyUsageQuota(data)); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); +}); + describe("cold partial writers hydrate only the surviving legacy cache", () => { for (const writerKind of ["parsed", "legacy"] as const) { for (const expired of [false, true]) { From 20c51bdd42f22836a2acdd55bfd565f015f45b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Wed, 23 Sep 2026 09:55:12 +0900 Subject: [PATCH 2/5] test(codex): clarify hard-lock window replacement assumptions --- .../ko/reference/cli/providers-accounts.md | 9 ++++-- .../docs/reference/cli/providers-accounts.md | 9 ++++-- src/codex/quota.ts | 5 +-- structure/providers/openai-tiers.md | 8 +++-- .../main-quota-evidence-validation.test.ts | 32 ++++++++----------- .../main-quota-provenance.test.ts | 26 +++++++++++++++ 6 files changed, 61 insertions(+), 28 deletions(-) diff --git a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md index 956d6713574..eea121ee750 100644 --- a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md @@ -88,9 +88,12 @@ ocx login anthropic 실제 사용량을 다시 확인하며, 조회 실패나 잘못된 수치는 차단을 풀지 않습니다. 일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다. -새 사용량 응답에서 현재 계정에 주간·월간 창만 존재한다고 명확히 확인되면, 오래된 5h 수치는 -제거하고 현재 창에 동일한 99% 기준을 적용합니다. 창 정보가 누락되거나 응답 헤더만 일부 -도착한 경우에는 이전 차단을 해제하지 않습니다. +새로운 유효한 WHAM 사용량 응답 한 건에서 1차 창의 기간이 **24시간 이상**으로 명시되고, +2차·3차 창이 없거나 그 기간도 24시간 이상으로 명시되면 이전 5h 수치를 대체합니다. +파서의 단기·장기 구분 기준을 따르므로 주간·월간뿐 아니라 하루짜리 창도 해당합니다. +현재 창에는 동일한 99% 기준을 적용합니다. 이 판단은 응답 한 건의 정보에 의존하며 연속 관측을 +요구하지 않습니다. 1차 창의 기간을 모르거나 응답 헤더만 일부 도착한 경우에는 이전 차단을 +해제하지 않습니다. 저장되는 옵션은 OpenCodex의 `config.json`에 있는 `"codexMainAccountHardLock": true`이며, 기본값은 꺼짐입니다. 식별된 메인 계정의 새 요청을 막는 기능이지 마지막 1%를 예약하는 기능은 diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 51e705b6ad0..9a54388df93 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -151,9 +151,12 @@ not erase an already measured blocking tuple. A predicted reset time alone does While blocked, the existing once-per-minute background cycle checks fresh owned usage; failed or invalid readings retain the block. Other pause, reauthentication, and upstream limits remain independent. -If a fresh usage response explicitly confirms that the account now has only weekly/monthly windows, -protection discards the obsolete 5h reading and evaluates the current window at the same 99% threshold. -Missing window metadata or partial response headers alone cannot clear a previous block. +Protection treats one fresh valid WHAM usage response as a replacement for the old 5h reading when +its primary window explicitly lasts **at least 24 hours** and secondary/tertiary windows are absent +or also explicitly last at least 24 hours. This follows the parser's short/long boundary, so a +one-day window qualifies as well as weekly/monthly windows. The current window still uses the same +99% threshold. This relies on the single reported snapshot; repeated observations are not required. +An unknown primary duration or partial response headers alone cannot clear a previous block. The persisted option is `"codexMainAccountHardLock": true` in OpenCodex's `config.json`; it is off by default. This protects new requests using the identified main account, not the last 1% itself: diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 37d3f9d4250..f6bfadc83ce 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -200,6 +200,7 @@ function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): bo && seconds >= MONTHLY_WINDOW_MIN_SECONDS; } +/** Same 24h short/long boundary as the parser; this includes a declared one-day window. */ function isExplicitLongWindow(window: WhamUsageWindow | null | undefined): boolean { const seconds = window?.limit_window_seconds; return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= WEEKLY_WINDOW_MIN_SECONDS; @@ -804,8 +805,8 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null; const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type)); const [primary, secondary, tertiary] = windows; - // A complete, valid long-window WHAM response can retire an old 5h policy tuple. - // The primary must declare its duration; absent secondary windows may be null or omitted. + // Policy treats one valid WHAM snapshot as a replacement when all declared windows are >=24h. + // This trusts the reported topology; absent secondary windows may be null or omitted. // Headers never supply this proof, and reset time alone still cannot release a block. if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary) && (secondary == null || isExplicitLongWindow(secondary)) diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 9810c976bda..e158c4753c8 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -301,13 +301,17 @@ release the block. Policy validation precedes legacy clamping. Supplementary mon become the fallback governing window without a monthly-only plan or explicit primary-monthly evidence. Previously unobserved usage is unknown, not fabricated headroom. -A fresh valid WHAM response with an explicitly long primary window can replace an obsolete +A single fresh valid WHAM response with an explicitly long primary window can replace an obsolete short-window tuple when secondary and tertiary windows are absent or also explicitly long. +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. An unknown primary duration, partial headers, or invalid usage cannot prove that the short window disappeared. Replacement proof belongs only to that observation and is never persisted; the resulting weekly/monthly window still blocks at 99%. This prevents old short-window exhaustion from surviving indefinitely on a now weekly/monthly account. Coverage lives in -`tests/codex-integration/main-quota-evidence-validation.test.ts` and +`tests/codex-integration/main-quota-evidence-validation.test.ts`, +`tests/codex-integration/main-quota-provenance.test.ts`, and `tests/codex-integration/main-account-hard-lock-recovery.test.ts`. The policy reads a separately retained identity-tagged quota snapshot, so the legacy rotation diff --git a/tests/codex-integration/main-quota-evidence-validation.test.ts b/tests/codex-integration/main-quota-evidence-validation.test.ts index 357a875af25..c3252711759 100644 --- a/tests/codex-integration/main-quota-evidence-validation.test.ts +++ b/tests/codex-integration/main-quota-evidence-validation.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { MAIN_CODEX_ACCOUNT_ID as MAIN } from "../../src/codex/account-id"; @@ -160,6 +160,19 @@ describe("main policy window replacement", () => { expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); } + test.each([86_399, 86_400, 86_401])("primary duration %s follows the exact 24h parser boundary", seconds => { + retainedShort(); + const data = { rate_limit: { + primary_window: { used_percent: 20, limit_window_seconds: seconds }, secondary_window: null, + } }; + const parsed = parseMainPolicyUsageQuota(data); + expect(parsed?.shortWindowAbsent).toBe(seconds >= 86_400 ? true : undefined); + publish(data); + expect(getMainPolicyQuota()?.shortPercent).toBe(seconds < 86_400 ? 20 : undefined); + expect(getMainPolicyQuota()?.weeklyPercent).toBe(seconds < 86_400 ? 35 : 20); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + }); + for (const [field, seconds] of [["weeklyPercent", weeklySeconds], ["monthlyPercent", monthlySeconds]] as const) { test.each([0, 35, 98.99, 99, 100])(`fresh ${field}=%s replaces a retired persisted short window`, percent => { retainedShort(); @@ -221,23 +234,6 @@ describe("main policy window replacement", () => { expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); }); - test("window replacement persists without carrying its proof into later partial updates", async () => { - retainedShort(); - publish({ rate_limit: { - primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, - } }); - await Bun.sleep(350); - const path = join(home, "codex-quota-cache.json"); - const persisted = readFileSync(path, "utf8"); - clearAccountQuota(); - writeFileSync(path, persisted); - expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); - expect(getMainPolicyQuota()).not.toHaveProperty("shortWindowAbsent"); - 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"); - }); - test("a superseded identity cannot retire the current account's short block", () => { const staleWriter = writerFor("fixture-main-b"); retainedShort(); diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index c6a26280a09..f2064fc5a56 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -28,10 +28,12 @@ import { getAccountQuota, getMainPolicyQuota, listAccountQuotas, + parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, updateAccountQuota, type StoredAccountQuota, + type WhamUsageResponse, } from "../../src/codex/quota"; import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { repoPath, repoRoot } from "../helpers/repo-root"; @@ -297,6 +299,30 @@ describe("main policy quota writes", () => { }); }); +test("window replacement persists without carrying its proof into later partial updates", () => { + const cfg = { codexMainAccountHardLock: true }; + const writer = writerFor(); + const publish = (data: WhamUsageResponse) => setAccountQuotaFromParsed( + MAIN, parseUsageQuota(data), undefined, writer, parseMainPolicyUsageQuota(data), + ); + setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, shortResetAt: 1 }, undefined, writer); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + publish({ rate_limit: { + 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(); + expect(JSON.parse(persisted).mainPolicyQuota.quota.weeklyPercent).toBe(35); + expect(persisted).not.toContain("shortWindowAbsent"); + clearAccountQuota(); + writeFileSync(join(testDir, "codex-quota-cache.json"), persisted); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + expect(getMainPolicyQuota()?.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"); +}); + describe("main policy quota durability and lifecycle", () => { // The first loop iteration is this graph's cold child; warm quota provenance imports before its // spawn timeout starts measuring the restart behavior. From 4bb53841a3a16da016d0886052313d5aa92cbf35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Wed, 23 Sep 2026 10:15:40 +0900 Subject: [PATCH 3/5] docs(codex): document quota publication and regression helpers --- src/codex/quota.ts | 17 +++++++++++++++-- .../main-quota-evidence-validation.test.ts | 3 +++ .../main-quota-provenance.test.ts | 3 +++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/codex/quota.ts b/src/codex/quota.ts index f6bfadc83ce..9129c65b3e8 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -274,6 +274,11 @@ function snapshotHasCustom(quota: Omit): boolea function snapshotHasUsage(quota: Omit): boolean { return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota); } +/** + * Publish parsed display quota and separately validated main-policy evidence after writer checks. + * A null policy observation retains only the matching main identity's previous evidence; + * transient replacement markers are consumed during merging and never enter stored snapshots. + */ export function setAccountQuotaFromParsed( accountId: string, quota: Omit | null, @@ -320,7 +325,11 @@ export function setAccountQuotaFromParsed( } } -/** One partial-window merge contract for legacy quota and identity-bound policy evidence. */ +/** + * Merge a partial observation into the legacy or identity-bound policy snapshot. + * Policy mode retains omitted blocking short usage unless this observation authorizes replacement; + * the returned snapshot contains quota fields only, without the transient replacement marker. + */ function mergeAccountQuota( quota: MainPolicyQuotaObservation, existing: StoredAccountQuota | undefined, @@ -799,7 +808,11 @@ function filterMainPolicyMonthlyQuota( return hasKnownQuotaValue(filtered) || filtered.resetCredits !== undefined ? filtered : null; } -/** Ordinary main policy rejects an entire message containing any invalid numeric window. */ +/** + * Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages. + * Mark a valid primary of at least 24h as replacement evidence only when all other declared + * windows are also at least 24h; null means this response supplies no usable policy observation. + */ 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; diff --git a/tests/codex-integration/main-quota-evidence-validation.test.ts b/tests/codex-integration/main-quota-evidence-validation.test.ts index c3252711759..8fe1248b29e 100644 --- a/tests/codex-integration/main-quota-evidence-validation.test.ts +++ b/tests/codex-integration/main-quota-evidence-validation.test.ts @@ -30,6 +30,7 @@ afterEach(() => { removeTreeWithRetry(home); }); +/** Observe a synthetic main identity and capture the live writer used to publish its fixture quota. */ function writerFor(accountId = "fixture-main-a") { observeMainQuotaIdentity(accountId); const writer = captureMainQuotaWriter(accountId); @@ -149,10 +150,12 @@ describe("main policy window replacement", () => { const weeklySeconds = 7 * 24 * 60 * 60; const monthlySeconds = 30 * 24 * 60 * 60; + /** Publish the same fixture through display normalization and strict policy validation. */ function publish(data: WhamUsageResponse) { setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, writerFor(), parseMainPolicyUsageQuota(data)); } + /** Hydrate a sixteen-day-old short-window block and assert the replacement test's initial state. */ function retainedShort() { const old = Date.now() - 16 * 24 * 60 * 60_000; writeColdPolicy({ shortPercent: 100, shortWindowSeconds: 18_000, diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index f2064fc5a56..188af39fb52 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -65,6 +65,7 @@ function installPersistenceClock() { }) as typeof setTimeout); } +/** Run the captured quota persistence callback and read its actual disk snapshot without a sleep. */ function flushPersistence(): string { if (!pendingPersist) throw new Error("Expected a scheduled quota persistence"); const pending = pendingPersist; @@ -74,6 +75,7 @@ function flushPersistence(): string { return readFileSync(join(testDir, "codex-quota-cache.json"), "utf8"); } +/** Bind a synthetic main identity and return its current generation-scoped quota writer. */ function writerFor(accountId = "fixture-main-a"): MainQuotaWriter { observeMainQuotaIdentity(accountId); const writer = captureMainQuotaWriter(accountId); @@ -302,6 +304,7 @@ describe("main policy quota writes", () => { test("window replacement persists without carrying its proof into later partial updates", () => { const cfg = { codexMainAccountHardLock: true }; const writer = writerFor(); + /** Publish both parsed projections with the captured writer throughout the simulated restart. */ const publish = (data: WhamUsageResponse) => setAccountQuotaFromParsed( MAIN, parseUsageQuota(data), undefined, writer, parseMainPolicyUsageQuota(data), ); From 4bef32e8c55a4abd513e874c5485cb0daabda99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Wed, 23 Sep 2026 10:40:48 +0900 Subject: [PATCH 4/5] docs(codex): complete quota recovery helper contracts --- src/codex/quota.ts | 5 +++++ .../main-account-hard-lock-recovery.test.ts | 8 ++++++++ tests/codex-integration/main-quota-provenance.test.ts | 6 ++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 9129c65b3e8..62942b6ae90 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -829,6 +829,11 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu return quota; } +/** + * Normalize WHAM windows into the display snapshot, preserving declared short-window shape. + * Finite percentages are clamped for compatibility; policy callers must validate raw readings + * separately. Return null when neither a quota value/window nor reset credits are available. + */ export function parseUsageQuota(data: WhamUsageResponse): Omit | null { const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number" ? data.rate_limit_reset_credits.available_count diff --git a/tests/codex-integration/main-account-hard-lock-recovery.test.ts b/tests/codex-integration/main-account-hard-lock-recovery.test.ts index d072ab7f898..6eb38582216 100644 --- a/tests/codex-integration/main-account-hard-lock-recovery.test.ts +++ b/tests/codex-integration/main-account-hard-lock-recovery.test.ts @@ -30,10 +30,12 @@ let previousHome: string | undefined; let previousCodexHome: string | undefined; let previousFetch: typeof fetch; +/** Build the minimal proxy configuration with main-account hard-lock recovery enabled. */ function config(): OcxConfig { return { port: 10100, defaultProvider: "openai", providers: {}, codexMainAccountHardLock: true }; } +/** Encode synthetic account and expiry claims for the fixture; this is not a signed credential. */ function bearer(expired = false): string { const payload = Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + (expired ? -120 : 86_400), @@ -42,6 +44,7 @@ function bearer(expired = false): string { return `header.${payload}.signature`; } +/** Write fixture credentials into the isolated home and reconcile the active main identity. */ function writeMain(expired = false): void { writeFileSync(join(home, "auth.json"), JSON.stringify({ tokens: { access_token: bearer(expired), refresh_token: "fixture-refresh", account_id: accountId, @@ -49,6 +52,7 @@ function writeMain(expired = false): void { reconcileMainCodexAccountRuntimeState(); } +/** Seed a 99% short-window block for the observed fixture identity, even though its reset elapsed. */ function block(): void { const writer = captureMainQuotaWriter(accountId); if (!writer) throw new Error("Fixture identity must be observed"); @@ -61,6 +65,10 @@ function usage(percent = 0): Response { } }); } +/** + * Stub recovery HTTP calls, requiring a known metadata/token URL and an active native-main drain. + * Return the captured URL list so tests can verify the requests made by background recovery. + */ function fetchWith(handler: (url: string, init?: RequestInit) => Promise) { const calls: string[] = []; globalThis.fetch = Object.assign(async (input: Parameters[0], init?: RequestInit) => { diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index 188af39fb52..01ef482339c 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -51,8 +51,10 @@ let previousCodexHome: string | undefined; let pendingPersist: { run: () => void; timer: ReturnType } | undefined; let timerSpy: ReturnType; -// Exercise the real debounced serializer deterministically, without sleeping or exporting -// a production flush hook. Only quota's 250ms timeout is captured; all others stay native. +/** + * Capture quota's 250ms persistence callback for explicit flushing; leave other timers native. + * Return the timer spy so teardown restores scheduling after exercising the real serializer. + */ function installPersistenceClock() { const nativeSetTimeout = globalThis.setTimeout; return spyOn(globalThis, "setTimeout").mockImplementation((( From 6ff5e04f69c69a1ebd2d50460449f9b52cc2b540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Wed, 23 Sep 2026 10:59:40 +0900 Subject: [PATCH 5/5] fix(codex): require explicit absent windows for lock replacement --- .../ko/reference/cli/providers-accounts.md | 6 +-- .../docs/reference/cli/providers-accounts.md | 4 +- src/codex/quota.ts | 12 +++--- structure/providers/openai-tiers.md | 4 +- .../main-account-hard-lock-recovery.test.ts | 2 +- .../main-quota-evidence-validation.test.ts | 42 ++++++++++++++----- .../main-quota-provenance.test.ts | 2 +- 7 files changed, 46 insertions(+), 26 deletions(-) diff --git a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md index eea121ee750..27d172b549f 100644 --- a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md @@ -89,11 +89,11 @@ ocx login anthropic 일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다. 새로운 유효한 WHAM 사용량 응답 한 건에서 1차 창의 기간이 **24시간 이상**으로 명시되고, -2차·3차 창이 없거나 그 기간도 24시간 이상으로 명시되면 이전 5h 수치를 대체합니다. +2차·3차 창이 명시적 `null`이거나 그 기간도 24시간 이상으로 명시되면 이전 5h 수치를 대체합니다. 파서의 단기·장기 구분 기준을 따르므로 주간·월간뿐 아니라 하루짜리 창도 해당합니다. 현재 창에는 동일한 99% 기준을 적용합니다. 이 판단은 응답 한 건의 정보에 의존하며 연속 관측을 -요구하지 않습니다. 1차 창의 기간을 모르거나 응답 헤더만 일부 도착한 경우에는 이전 차단을 -해제하지 않습니다. +요구하지 않습니다. 2차·3차 필드가 생략되었거나, 1차 창의 기간을 모르거나, 응답 헤더만 일부 +도착한 경우에는 이전 차단을 해제하지 않습니다. 저장되는 옵션은 OpenCodex의 `config.json`에 있는 `"codexMainAccountHardLock": true`이며, 기본값은 꺼짐입니다. 식별된 메인 계정의 새 요청을 막는 기능이지 마지막 1%를 예약하는 기능은 diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 9a54388df93..dd6dfae95ce 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -152,11 +152,11 @@ While blocked, the existing once-per-minute background cycle checks fresh owned invalid readings retain the block. Other pause, reauthentication, and upstream limits remain independent. Protection treats one fresh valid WHAM usage response as a replacement for the old 5h reading when -its primary window explicitly lasts **at least 24 hours** and secondary/tertiary windows are absent +its primary window explicitly lasts **at least 24 hours** and secondary/tertiary windows are explicitly `null` or also explicitly last at least 24 hours. This follows the parser's short/long boundary, so a one-day window qualifies as well as weekly/monthly windows. The current window still uses the same 99% threshold. This relies on the single reported snapshot; repeated observations are not required. -An unknown primary duration or partial response headers alone cannot clear a previous block. +Omitted secondary/tertiary fields, an unknown primary duration, or partial response headers cannot clear a previous block. The persisted option is `"codexMainAccountHardLock": true` in OpenCodex's `config.json`; it is off by default. This protects new requests using the identified main account, not the last 1% itself: diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 62942b6ae90..276d3df02c7 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -810,20 +810,20 @@ function filterMainPolicyMonthlyQuota( /** * Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages. - * Mark a valid primary of at least 24h as replacement evidence only when all other declared - * windows are also at least 24h; null means this response supplies no usable policy observation. + * Mark a valid primary of at least 24h as replacement evidence only when both other windows + * are explicitly null or at least 24h. A null result supplies no usable policy observation. */ 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; - // Policy treats one valid WHAM snapshot as a replacement when all declared windows are >=24h. - // This trusts the reported topology; absent secondary windows may be null or omitted. + // WHAM explicitly reports absent windows as null; omissions cannot prove replacement. + // Policy trusts one complete snapshot only when every non-null window is >=24h. // Headers never supply this proof, and reset time alone still cannot release a block. if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary) - && (secondary == null || isExplicitLongWindow(secondary)) - && (tertiary == null || isExplicitLongWindow(tertiary))) { + && (secondary === null || isExplicitLongWindow(secondary)) + && (tertiary === null || isExplicitLongWindow(tertiary))) { return { ...quota, shortWindowAbsent: true }; } return quota; diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index e158c4753c8..000727ac2d9 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -302,11 +302,11 @@ become the fallback governing window without a monthly-only plan or explicit pri Previously unobserved usage is unknown, not fabricated headroom. A single fresh valid WHAM response with an explicitly long primary window can replace an obsolete -short-window tuple when secondary and tertiary windows are absent or also explicitly long. +short-window tuple when secondary and tertiary windows are explicitly null or also explicitly long. 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. -An unknown primary duration, partial headers, or invalid usage cannot prove that the +Omitted secondary/tertiary fields, an unknown primary duration, partial headers, or invalid usage cannot prove that the short window disappeared. Replacement proof belongs only to that observation and is never persisted; the resulting weekly/monthly window still blocks at 99%. This prevents old short-window exhaustion from surviving indefinitely on a now weekly/monthly account. Coverage lives in diff --git a/tests/codex-integration/main-account-hard-lock-recovery.test.ts b/tests/codex-integration/main-account-hard-lock-recovery.test.ts index 6eb38582216..5517374e4de 100644 --- a/tests/codex-integration/main-account-hard-lock-recovery.test.ts +++ b/tests/codex-integration/main-account-hard-lock-recovery.test.ts @@ -133,7 +133,7 @@ afterEach(async () => { describe("main hard-lock background recovery", () => { test("owned metadata recovery replaces an obsolete short block with the current weekly window", async () => { const calls = fetchWith(async () => Response.json({ plan_type: "pro", rate_limit: { - primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, + primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, tertiary_window: null, } })); await runMainAccountHardLockRecovery(config()); expect(calls).toEqual([whamUrl]); diff --git a/tests/codex-integration/main-quota-evidence-validation.test.ts b/tests/codex-integration/main-quota-evidence-validation.test.ts index 8fe1248b29e..2ca37630c7a 100644 --- a/tests/codex-integration/main-quota-evidence-validation.test.ts +++ b/tests/codex-integration/main-quota-evidence-validation.test.ts @@ -166,7 +166,7 @@ describe("main policy window replacement", () => { test.each([86_399, 86_400, 86_401])("primary duration %s follows the exact 24h parser boundary", seconds => { retainedShort(); const data = { rate_limit: { - primary_window: { used_percent: 20, limit_window_seconds: seconds }, secondary_window: null, + primary_window: { used_percent: 20, limit_window_seconds: seconds }, secondary_window: null, tertiary_window: null, } }; const parsed = parseMainPolicyUsageQuota(data); expect(parsed?.shortWindowAbsent).toBe(seconds >= 86_400 ? true : undefined); @@ -180,7 +180,7 @@ describe("main policy window replacement", () => { test.each([0, 35, 98.99, 99, 100])(`fresh ${field}=%s replaces a retired persisted short window`, percent => { retainedShort(); publish({ rate_limit: { - primary_window: { used_percent: percent, limit_window_seconds: seconds }, secondary_window: null, + primary_window: { used_percent: percent, limit_window_seconds: seconds }, secondary_window: null, tertiary_window: null, } }); const policy = getMainPolicyQuota(); expect(policy?.[field]).toBe(percent); @@ -197,12 +197,12 @@ describe("main policy window replacement", () => { test.each([ { primary_window: { used_percent: 35 } }, - { primary_window: { limit_window_seconds: weeklySeconds }, secondary_window: null }, - { primary_window: { used_percent: -1, limit_window_seconds: weeklySeconds }, secondary_window: null }, - { primary_window: { used_percent: 101, limit_window_seconds: weeklySeconds }, secondary_window: null }, - { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: {} }, + { primary_window: { limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null }, + { primary_window: { used_percent: -1, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null }, + { primary_window: { used_percent: 101, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: {}, tertiary_window: null }, { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, - secondary_window: { used_percent: 99, limit_window_seconds: 18_000 } }, + secondary_window: { used_percent: 99, limit_window_seconds: 18_000 }, tertiary_window: null }, { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: { used_percent: 99, limit_window_seconds: 18_000 } }, ])("partial, invalid or short-bearing metadata retains the old block: %j", rate_limit => { @@ -216,15 +216,35 @@ describe("main policy window replacement", () => { retainedShort(); publish({ rate_limit: { primary_window: { used_percent: 35, limit_window_seconds: monthlySeconds }, - secondary_window: { used_percent: 99, limit_window_seconds: weeklySeconds }, + secondary_window: { used_percent: 99, limit_window_seconds: weeklySeconds }, tertiary_window: null, } }); expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); }); - test("a valid long primary also proves replacement when absent secondary is omitted", () => { + test.each([ + {}, + { secondary_window: null }, + { tertiary_window: null }, + { secondary_window: { used_percent: 20, limit_window_seconds: weeklySeconds } }, + { tertiary_window: { used_percent: 20, limit_window_seconds: monthlySeconds } }, + ])("omitted secondary or tertiary windows cannot retire a short block: %j", windows => { retainedShort(); - publish({ rate_limit: { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds } } }); + const data = { rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, ...windows, + } }; + expect(parseMainPolicyUsageQuota(data)?.shortWindowAbsent).toBeUndefined(); + publish(data); + expect(getMainPolicyQuota()?.shortPercent).toBe(100); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("an explicit null secondary and long tertiary permit replacement", () => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + tertiary_window: { used_percent: 20, limit_window_seconds: monthlySeconds }, + } }); expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); }); @@ -241,7 +261,7 @@ describe("main policy window replacement", () => { const staleWriter = writerFor("fixture-main-b"); retainedShort(); const data = { rate_limit: { - primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null, } }; setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, staleWriter, parseMainPolicyUsageQuota(data)); expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index 01ef482339c..e132854b38f 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -313,7 +313,7 @@ test("window replacement persists without carrying its proof into later partial setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, shortResetAt: 1 }, undefined, writer); expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); publish({ rate_limit: { - primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, + primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, tertiary_window: null, } }); // Execute quota's actual debounced serializer through the existing deterministic clock. const persisted = flushPersistence();