Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,19 @@ Reserve입니다. 차단 중에는 그 메인 계정의 Reserve를 활성화할
예정된 리셋 시간이 지났다는 이유만으로 풀지는 않습니다. 차단 중에는 1분 주기 점검이
알려진 차단 창의 리셋 시각까지 기다린 뒤 계정의 실제 사용량을 확인합니다. 이후에도 차단 상태이거나
미래 리셋 시각을 모르면 5·10·20·40·60분 간격으로 재시도하며, 더 긴 `Retry-After`가 있으면
프로필 및 토큰 준비도 그 시각까지 미룹니다. 유효한 최신 수치만 차단을 해제합니다.
프로필 및 토큰 준비도 그 시각까지 미룹니다. 유효한 최신 수치나 창이 없음을 명시한 응답만 차단을 해제합니다.
Pool 모드에서 사용량 조회의 `--refresh`는 캐시 유효기간을 무시하지만 실패 후 대기 시간은 지킵니다.
조회가 연기되면 새 진단 시도로 기록하지 않습니다.
일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다.

새로운 유효한 WHAM 사용량 응답 한 건에서 1차 창의 기간이 **24시간 이상**으로 명시되고 유효한 사용량 수치가 있으며,
2차·3차 창이 명시적 `null`이거나 그 기간도 24시간 이상으로 명시되고 사용량 수치도 함께 오면 이전 5h 수치를 대체합니다.
새로운 유효한 WHAM 응답에서 1차 창이 **24시간 이상**이고 사용량 수치가 있으면 이전 5h 수치를 대체할 수 있습니다.
1차 창이 명시적 `null`이고 2차 창에 유효한 주간 사용량이 있어도 같습니다. 어느 경우든 2차·3차 창은
명시적 `null`이거나 기간이 24시간 이상이고 유효한 사용량 수치가 있어야 합니다.
파서의 단기·장기 구분 기준을 따르므로 주간·월간뿐 아니라 하루짜리 창도 해당합니다.
현재 창에는 동일한 98% 기준을 적용합니다. 이 판단은 응답 한 건의 정보에 의존하며 연속 관측을
요구하지 않습니다. 2차·3차 필드가 생략되었거나, 1차 창의 기간을 모르거나, 응답 헤더만 일부
도착한 경우에는 이전 차단을 해제하지 않습니다.
요구하지 않습니다. 창 필드가 하나라도 생략되었거나, 창의 기간을 모르거나, 응답 헤더만 일부
도착한 경우에는 이전 차단을 해제하지 않습니다. 모든 창이 `null`이고 크레딧만 있거나, 보조 월간 수치만
있는 응답도 차단을 풀지 않습니다.
지연 응답을 반영하기 전에 저장된 인증정보를 다시 확인합니다. 파일을 읽을 수 없거나 같은 계정의 인증 토큰이
교체되었다면 별도 사용량 조회가 없어도 이전 응답은 사용량 캐시나 차단 상태를 갱신하거나 새 토큰을 재인증 대상으로 표시하지 않습니다.
해당 요청자에게 파싱된 조회 결과를 반환할 수는 있지만, 공유 상태나 차단 해제 근거에는 반영하지 않습니다.
Expand Down
10 changes: 6 additions & 4 deletions docs-site/src/content/docs/reference/cli/providers-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,18 @@ An unreadable 5h reading cannot hide a weekly block. Unknown usage does not fabr
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 a fresh valid reading
schedule; 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.

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 explicitly `null`
or also explicitly last at least 24 hours and report their usage. This follows the parser's short/long boundary, so a
its primary window explicitly lasts **at least 24 hours** and reports valid usage, or its primary window is
explicitly `null` and a measured secondary supplies the weekly usage. In either case, secondary/tertiary
windows must be explicitly `null` or explicitly last at least 24 hours and report valid usage. 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
98% threshold. This relies on the single reported snapshot; repeated observations are not required.
Omitted secondary/tertiary fields, an unknown primary duration, or partial response headers cannot clear a previous block.
Any omitted window field, an unknown duration, or partial response headers cannot clear a previous block.
All-null responses carrying only credits and supplementary monthly-only readings also cannot establish recovery.
The proxy checks the stored credential again before applying a delayed response. An unreadable file
or replaced bearer cannot update the usage cache, release the lock, or quarantine the new credential,
even for the same account with no second quota read.
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,7 @@
"main-device-reauth-ui.test.ts": "gui",
"main-device-reauth.test.ts": "codex-integration",
"main-quota-evidence-validation.test.ts": "codex-integration",
"main-account-hard-lock-retirement.test.ts": "codex-integration",
"main-quota-provenance.test.ts": "codex-integration",
"main-quota-window-observation.test.ts": "codex-integration",
"management-anthropic-reset-grants.test.ts": "server",
Expand Down
4 changes: 2 additions & 2 deletions src/codex/main-account-hard-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ export function getMainAccountHardLockStatus(
const windows = governingWindows(quota);
const blocking = windows.filter(w => validPercent(w.percent) && w.percent >= MAIN_ACCOUNT_HARD_LOCK_PERCENT);
if (blocking.length > 0) {
// The lock holds until every blocking window reads lower, so the earliest possible unlock is
// The lock holds until every blocking window reads lower or is authoritatively absent, so the earliest possible unlock is
// the latest blocking reset. One blocking window without a future reset makes it unknowable.
// A predicted reset is not evidence of recovery either way: only a fresh lower reading releases.
// A predicted reset is not evidence of recovery; fresh lower usage or validated WHAM absence releases.
const resets = blocking.map(w => resetTimestamp(w.resetAt));
const resetAt = resets.every(r => r !== undefined && r > now) ? Math.max(...(resets as number[])) : undefined;
return { enabled: true, state: "blocked", ...(resetAt !== undefined ? { resetAt } : {}) };
Expand Down
7 changes: 4 additions & 3 deletions src/codex/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,8 +827,8 @@ 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 both other windows
* are explicitly null or at least 24h. A null result supplies no usable policy observation.
* 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.
*/
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null {
const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
Expand All @@ -840,7 +840,8 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu
// 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 && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary)
if (quota && (isMeasuredLongWindow(primary)
|| (primary === null && isMeasuredLongWindow(secondary) && quota.weeklyPercent !== undefined))
Comment on lines +843 to +844

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retire the legacy short tuple with the policy evidence

When the retained blocking short reading has no reset timestamp, or its reset is still in the future, this new shortWindowAbsent marker clears only mainPolicyQuota: setAccountQuotaFromParsed still merges the unmarked parseUsageQuota result into the ordinary accountQuota, whose assignCarriedShort path preserves that short tuple. Consequently getMainAccountHardLockStatus becomes ready while computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID)) continues to include the stale 98–100% short value, so Pool routing and auto-switching can keep avoiding the main account indefinitely despite the authoritative-null response. Consume the validated absence marker in the main account's ordinary merge as well, with coverage for missing and future short resets.

Useful? React with 👍 / 👎.

&& (secondary === null || isMeasuredLongWindow(secondary))
&& (tertiary === null || isMeasuredLongWindow(tertiary))) {
return { ...quota, shortWindowAbsent: true };
Expand Down
20 changes: 11 additions & 9 deletions structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ This stops partial weekly/Spark or credits-only refreshes from renewing obsolete
The separately retained main-policy snapshot preserves omitted blocking short evidence even after
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. A validated long-primary WHAM snapshot can also retire the short tuple as described below.
it. A validated complete 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
Expand All @@ -320,14 +320,14 @@ Regression coverage lives in `tests/codex-integration/codex-quota-parser-parity.
`MAIN_ACCOUNT_HARD_LOCK_PERCENT` = 98%. The 5h/short window and the weekly window each govern on
their own: either one at 98% blocks, and an unknown or invalid reading in one never hides a block
in the other (unknown still admits). Monthly governs only a monthly-only account. A block holds
until every blocking window reads lower, so its reported `resetAt` is the latest blocking reset,
until every blocking window reads lower or is authoritatively absent, so `resetAt` is the latest blocking reset,
omitted when any blocking window has none. In the policy snapshot a reset-only weekly observation
keeps a blocking weekly tuple, mirroring the short-window rule; monthly-primary evidence still replaces it.
It blocks newly admitted identity-matched main-account requests. Pool alternatives remain eligible;
explicit main selection and stored Direct substitution do not override it. It neither pauses the
account nor clears upstream cooldown/reauth state, and management quota refresh remains available.
Only a fresh valid reading below 98%, including 0%, releases a measured block; passing a reset
timestamp alone does not. The minute sweep waits locally until the latest known blocking reset;
Fresh valid usage below 98%, including 0%, or validated WHAM absence retires a measured short block;
passing a reset timestamp alone does not. The minute sweep waits locally until the latest known blocking reset;
when no future reset is known or reads remain blocked, main recovery uses the same capped
5/10/20/40/60-minute delay calculation as usage-query failures. Skipped ticks do not extend it;
the physical bearer is reconciled before checking the delay, and late results cannot charge
Expand All @@ -336,7 +336,7 @@ current credential before the recovery worker takes a profile lease or prepares
credential has a separate key and may proceed immediately.
Nonterminal 401/403 responses do not arm the successful-but-blocked recovery delay; the next
sweep may retry, while terminal authentication failure keeps its reauth quarantine.
Only fresh lower usage releases the lock; no inference or reset-credit consumption is added. Failed,
Only fresh lower usage or validated window absence releases the lock; no reset-credit consumption is added. Failed,
missing, non-finite or out-of-range readings do not release the block. Policy validation precedes
legacy clamping. Supplementary monthly data cannot become the fallback governing window without a
monthly-only plan or explicit primary-monthly evidence. Previously unobserved usage is unknown, not
Expand All @@ -361,16 +361,18 @@ boundary, the admission consequence, and the settings opt-out round trip are cov
hard-lock tests registered in `scripts/test-layout/layout.json`, including
`tests/config/settings-main-account-hard-lock.test.ts`.

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 explicitly null or also explicitly long with a valid usage reading.
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.
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.
Omitted secondary/tertiary fields, a long auxiliary window without a usage reading, 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;
Any omitted window field, an unreadable long window, an 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 98%. 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`,
`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`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ 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, tertiary_window: null,
primary_window: null, secondary_window: { used_percent: 35, limit_window_seconds: 604_800 }, tertiary_window: null,
} }));
await runMainAccountHardLockRecovery(config());
expect(calls).toEqual([whamUrl]);
Expand Down
Loading
Loading