@bitkyc08/opencodex 2.42.0 → 2.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
package/src/codex/quota.ts
CHANGED
|
@@ -3,40 +3,13 @@ import { join } from "node:path";
|
|
|
3
3
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
4
4
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
5
5
|
import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
6
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
7
|
+
import { getObservedMainQuotaIdentityKey, isMainQuotaWriterLive, type MainQuotaWriter } from "./main-account-cache";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
monthlyPercent?: number;
|
|
10
|
-
weeklyResetAt?: number;
|
|
11
|
-
monthlyResetAt?: number;
|
|
12
|
-
/**
|
|
13
|
-
* A sub-day burst window, when upstream declares one (#1791).
|
|
14
|
-
*
|
|
15
|
-
* K12 and similar plans enforce a rolling 5-hour limit ALONGSIDE the weekly one.
|
|
16
|
-
* Not folding it into `weeklyPercent` stopped the mislabeling, but dropping it
|
|
17
|
-
* entirely hides a limit that genuinely blocks the account: a 429 at 100% here is
|
|
18
|
-
* real even while the weekly quota is untouched.
|
|
19
|
-
*
|
|
20
|
-
* `shortWindowSeconds` is retained because the duration is the only thing that makes
|
|
21
|
-
* this window self-describing; the slot it arrived in is not stable across plans.
|
|
22
|
-
*/
|
|
23
|
-
shortPercent?: number;
|
|
24
|
-
shortResetAt?: number;
|
|
25
|
-
shortWindowSeconds?: number;
|
|
26
|
-
customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
|
|
27
|
-
resetCredits?: number;
|
|
28
|
-
/**
|
|
29
|
-
* True when `monthlyPercent` came from an explicitly-monthly PRIMARY window —
|
|
30
|
-
* i.e. it is the account's governing quota reading, not a supplementary
|
|
31
|
-
* tertiary window. Tertiary-only monthly data lands in the same field but says
|
|
32
|
-
* nothing about the weekly quota that actually gates a non-Go/Free account,
|
|
33
|
-
* so recovery must be able to tell the two apart (#967 audit).
|
|
34
|
-
*/
|
|
35
|
-
monthlyIsPrimaryWindow?: boolean;
|
|
36
|
-
updatedAt: number;
|
|
37
|
-
};
|
|
9
|
+
import type { StoredAccountQuota, WhamUsageResponse, WhamUsageWindow } from "./quota-types";
|
|
10
|
+
export type { StoredAccountQuota, WhamUsageResponse } from "./quota-types";
|
|
38
11
|
|
|
39
|
-
/** Disk snapshot under OPENCODEX_HOME —
|
|
12
|
+
/** Disk snapshot under OPENCODEX_HOME — quota and policy identity only, never credential tags. */
|
|
40
13
|
const QUOTA_CACHE_FILENAME = "codex-quota-cache.json";
|
|
41
14
|
/** Keep last-known bars across restarts; WHAM still refreshes on TTL in live/prime paths. */
|
|
42
15
|
const QUOTA_DISK_MAX_AGE_MS = 6 * 60 * 60_000;
|
|
@@ -45,41 +18,14 @@ const QUOTA_PERSIST_DEBOUNCE_MS = 250;
|
|
|
45
18
|
type QuotaDiskFile = {
|
|
46
19
|
version: 1;
|
|
47
20
|
quotas: Record<string, StoredAccountQuota>;
|
|
21
|
+
mainPolicyQuota?: MainPolicyQuota;
|
|
48
22
|
};
|
|
49
23
|
|
|
24
|
+
type MainPolicyQuota = { identityKey: string; quota: StoredAccountQuota };
|
|
25
|
+
let mainPolicyQuota: MainPolicyQuota | null = null;
|
|
50
26
|
let diskHydrated = false;
|
|
51
27
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
52
28
|
|
|
53
|
-
export type WhamUsageResponse = {
|
|
54
|
-
email?: string | null;
|
|
55
|
-
plan_type?: unknown;
|
|
56
|
-
rate_limit?: {
|
|
57
|
-
// Live WHAM payloads send explicit nulls for absent windows (issue #315 repro).
|
|
58
|
-
primary_window?: WhamUsageWindow | null;
|
|
59
|
-
secondary_window?: WhamUsageWindow | null;
|
|
60
|
-
tertiary_window?: WhamUsageWindow | null;
|
|
61
|
-
};
|
|
62
|
-
rate_limit_reset_credits?: {
|
|
63
|
-
available_count: number;
|
|
64
|
-
} | null;
|
|
65
|
-
additional_rate_limits?: WhamAdditionalRateLimit[] | null;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type WhamAdditionalRateLimit = {
|
|
69
|
-
limit_name?: unknown;
|
|
70
|
-
metered_feature?: unknown;
|
|
71
|
-
rate_limit?: {
|
|
72
|
-
primary_window?: WhamUsageWindow | null;
|
|
73
|
-
secondary_window?: WhamUsageWindow | null;
|
|
74
|
-
} | null;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
type WhamUsageWindow = {
|
|
78
|
-
used_percent?: number;
|
|
79
|
-
reset_at?: number;
|
|
80
|
-
limit_window_seconds?: number;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
29
|
const MONTHLY_WINDOW_MIN_SECONDS = 28 * 24 * 60 * 60;
|
|
84
30
|
/**
|
|
85
31
|
* Shortest window still plausibly the WEEKLY quota (#1791).
|
|
@@ -195,6 +141,16 @@ export function normalizeUsagePercent(value: unknown): number | undefined {
|
|
|
195
141
|
return Math.max(0, Math.min(100, numeric));
|
|
196
142
|
}
|
|
197
143
|
|
|
144
|
+
/** Reject numeric policy evidence before legacy clamping can fabricate a valid reading. */
|
|
145
|
+
function isInvalidPolicyUsagePercent(value: unknown): boolean {
|
|
146
|
+
if (typeof value === "number") return !Number.isFinite(value) || value < 0 || value > 100;
|
|
147
|
+
if (typeof value !== "string" || value.trim() === "") return false;
|
|
148
|
+
const numeric = Number(value);
|
|
149
|
+
// Non-numeric metadata stays unknown; explicit nonfinite spellings are invalid evidence.
|
|
150
|
+
if (Number.isNaN(numeric)) return /^[+-]?(?:nan|infinity)$/i.test(value.trim());
|
|
151
|
+
return !Number.isFinite(numeric) || numeric < 0 || numeric > 100;
|
|
152
|
+
}
|
|
153
|
+
|
|
198
154
|
function normalizeResetAt(value: unknown): number | undefined {
|
|
199
155
|
const numeric = typeof value === "number"
|
|
200
156
|
? value
|
|
@@ -208,6 +164,8 @@ function normalizeResetAt(value: unknown): number | undefined {
|
|
|
208
164
|
function hasKnownQuotaValue(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
209
165
|
return [quota.weeklyPercent, quota.monthlyPercent, quota.shortPercent]
|
|
210
166
|
.some(value => typeof value === "number" && Number.isFinite(value))
|
|
167
|
+
// Known short-window shape with unknown usage still selects that window for policy.
|
|
168
|
+
|| snapshotHasShort(quota)
|
|
211
169
|
|| !!quota.customWindows?.some(window => Number.isFinite(window.percent));
|
|
212
170
|
}
|
|
213
171
|
|
|
@@ -274,11 +232,48 @@ export function setAccountQuotaFromParsed(
|
|
|
274
232
|
accountId: string,
|
|
275
233
|
quota: Omit<StoredAccountQuota, "updatedAt"> | null,
|
|
276
234
|
writerGeneration = captureConfigGeneration(),
|
|
235
|
+
mainWriter?: MainQuotaWriter,
|
|
236
|
+
policyQuota: Omit<StoredAccountQuota, "updatedAt"> | null = quota,
|
|
277
237
|
): void {
|
|
278
238
|
if (!quota) return;
|
|
279
239
|
if (!mayCommitAccountQuota(accountId, writerGeneration)) return;
|
|
280
|
-
const
|
|
281
|
-
|
|
240
|
+
const isMain = accountId === MAIN_CODEX_ACCOUNT_ID;
|
|
241
|
+
if (isMain && mainWriter && !isMainQuotaWriterLive(mainWriter)) return;
|
|
242
|
+
hydrateAccountQuotasFromDisk();
|
|
243
|
+
const legacyExisting = accountQuota.get(accountId);
|
|
244
|
+
const updatedAt = Date.now();
|
|
245
|
+
// Legacy rotation keeps its existing carry behavior, but never inherits policy-only
|
|
246
|
+
// evidence that outlived its disk TTL. Policy has a separate, identity-checked base.
|
|
247
|
+
const next = mergeAccountQuota(quota, legacyExisting, updatedAt);
|
|
248
|
+
accountQuota.set(accountId, next);
|
|
249
|
+
if (isMain) {
|
|
250
|
+
const policyExisting = mainWriter && mainPolicyQuota?.identityKey === mainWriter.identityKey
|
|
251
|
+
? mainPolicyQuota.quota
|
|
252
|
+
: undefined;
|
|
253
|
+
mainPolicyQuota = mainWriter && (policyQuota || policyExisting)
|
|
254
|
+
? {
|
|
255
|
+
identityKey: mainWriter.identityKey,
|
|
256
|
+
quota: policyQuota
|
|
257
|
+
? structuredClone(mergeAccountQuota(policyQuota, policyExisting, updatedAt, true))
|
|
258
|
+
: policyExisting!,
|
|
259
|
+
}
|
|
260
|
+
: null;
|
|
261
|
+
}
|
|
262
|
+
schedulePersistAccountQuotas();
|
|
263
|
+
// Credits carry the previous usage tuple; they must not refresh its observation clock.
|
|
264
|
+
if (!(quota.resetCredits !== undefined && !snapshotHasUsage(quota))) {
|
|
265
|
+
notifyCodexQuotaSnapshot(accountId, next);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** One partial-window merge contract for legacy quota and identity-bound policy evidence. */
|
|
270
|
+
function mergeAccountQuota(
|
|
271
|
+
quota: Omit<StoredAccountQuota, "updatedAt">,
|
|
272
|
+
existing: StoredAccountQuota | undefined,
|
|
273
|
+
updatedAt: number,
|
|
274
|
+
policyEvidence = false,
|
|
275
|
+
): StoredAccountQuota {
|
|
276
|
+
const next: StoredAccountQuota = { updatedAt };
|
|
282
277
|
const creditsOnly = quota.resetCredits !== undefined && !snapshotHasUsage(quota);
|
|
283
278
|
|
|
284
279
|
if (creditsOnly) {
|
|
@@ -288,20 +283,21 @@ export function setAccountQuotaFromParsed(
|
|
|
288
283
|
if (existing?.monthlyResetAt !== undefined) next.monthlyResetAt = existing.monthlyResetAt;
|
|
289
284
|
if (existing?.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
290
285
|
if (existing?.shortPercent !== undefined) next.shortPercent = existing.shortPercent;
|
|
286
|
+
if (existing?.shortObservedAt !== undefined) next.shortObservedAt = existing.shortObservedAt;
|
|
291
287
|
if (existing?.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
292
288
|
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
293
289
|
if (existing?.customWindows !== undefined) next.customWindows = existing.customWindows;
|
|
294
290
|
next.resetCredits = quota.resetCredits;
|
|
295
|
-
|
|
296
|
-
schedulePersistAccountQuotas();
|
|
297
|
-
return;
|
|
291
|
+
return next;
|
|
298
292
|
}
|
|
299
293
|
|
|
300
294
|
if (snapshotHasWeekly(quota)) {
|
|
301
295
|
if (quota.weeklyPercent !== undefined) next.weeklyPercent = quota.weeklyPercent;
|
|
302
296
|
if (quota.weeklyResetAt !== undefined) next.weeklyResetAt = quota.weeklyResetAt;
|
|
303
|
-
} else if (snapshotHasMonthly(quota)
|
|
304
|
-
|
|
297
|
+
} else if (snapshotHasMonthly(quota)
|
|
298
|
+
&& (!policyEvidence || quota.monthlyIsPrimaryWindow === true)) {
|
|
299
|
+
// Legacy monthly-only clearing is unchanged (#382). Policy needs a governing
|
|
300
|
+
// monthly-primary observation: a tertiary-only header cannot retract weekly99.
|
|
305
301
|
} else if (existing?.weeklyPercent !== undefined) {
|
|
306
302
|
next.weeklyPercent = existing.weeklyPercent;
|
|
307
303
|
if (existing.weeklyResetAt !== undefined) next.weeklyResetAt = existing.weeklyResetAt;
|
|
@@ -322,14 +318,19 @@ export function setAccountQuotaFromParsed(
|
|
|
322
318
|
if (existing.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
323
319
|
}
|
|
324
320
|
|
|
325
|
-
|
|
326
|
-
|
|
321
|
+
const preserveKnownShort = policyEvidence && quota.shortPercent === undefined && finitePercent(existing?.shortPercent);
|
|
322
|
+
if (snapshotHasShort(quota) && !preserveKnownShort) {
|
|
323
|
+
if (quota.shortPercent !== undefined) {
|
|
324
|
+
next.shortPercent = quota.shortPercent;
|
|
325
|
+
if (Number.isFinite(quota.shortPercent)) next.shortObservedAt = next.updatedAt;
|
|
326
|
+
}
|
|
327
327
|
if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt;
|
|
328
328
|
if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds;
|
|
329
329
|
} else {
|
|
330
|
-
//
|
|
331
|
-
//
|
|
330
|
+
// Unknown usage is not a lower reading. Retain the entire known tuple: pairing
|
|
331
|
+
// its percentage with new metadata would silently extend or shorten its reset.
|
|
332
332
|
if (existing?.shortPercent !== undefined) next.shortPercent = existing.shortPercent;
|
|
333
|
+
if (existing?.shortObservedAt !== undefined) next.shortObservedAt = existing.shortObservedAt;
|
|
333
334
|
if (existing?.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
334
335
|
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
335
336
|
}
|
|
@@ -339,8 +340,68 @@ export function setAccountQuotaFromParsed(
|
|
|
339
340
|
if (quota.resetCredits !== undefined) next.resetCredits = quota.resetCredits;
|
|
340
341
|
else if (existing?.resetCredits !== undefined) next.resetCredits = existing.resetCredits;
|
|
341
342
|
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
return next;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Hand a committed snapshot to the optional quota-reset observer.
|
|
348
|
+
*
|
|
349
|
+
* Lazy import on purpose. This file is statically reachable from
|
|
350
|
+
* src/server/responses/core.ts (via codex/auth-context.ts), and
|
|
351
|
+
* applyAccountQuotaFromUpstreamHeaders runs it once per pooled response. A static import
|
|
352
|
+
* would load the observer, its config resolution, and its sink registry into every install,
|
|
353
|
+
* including installs that never enable the feature — the same failure mode
|
|
354
|
+
* tests/core-lab-boundary.test.ts exists to prevent for src/lab/.
|
|
355
|
+
*
|
|
356
|
+
* The previous snapshot is deliberately NOT passed. The observer keeps its own persisted
|
|
357
|
+
* baseline (see swapLastObservedWindows), so every committed write must reach it — including
|
|
358
|
+
* the first one, which is what establishes that baseline. An earlier version of this
|
|
359
|
+
* function skipped the call when the in-map `existing` was undefined; that left the observer
|
|
360
|
+
* with no baseline to compare against, so the write AFTER a rollover was silently treated as
|
|
361
|
+
* the first observation and no reset ever fired.
|
|
362
|
+
*
|
|
363
|
+
* The snapshot is COPIED synchronously, before the import resolves. `next` is the live map
|
|
364
|
+
* value and the next write mutates it, so an observation that read it after awaiting could
|
|
365
|
+
* see a later snapshot than the one it was called for.
|
|
366
|
+
*
|
|
367
|
+
* Observations are SERIALIZED through a module-level promise chain, because the baseline
|
|
368
|
+
* swap must happen in call order. An earlier version awaited two imports and then swapped,
|
|
369
|
+
* and Bun does not resolve concurrent import() calls in call order: a burst of 21
|
|
370
|
+
* rising-usage writes (10% -> 90%, no reset at all) arrived reordered and fired a false
|
|
371
|
+
* "surprise" reset on every run. Worse, the false event CLAIMED the durable idempotence
|
|
372
|
+
* key, so the genuine reset on that window was then permanently suppressed.
|
|
373
|
+
*
|
|
374
|
+
* `pendingObservation` is reassigned SYNCHRONOUSLY here, so each link is queued in call
|
|
375
|
+
* order and only starts once the previous one has committed its baseline. A FIFO queue
|
|
376
|
+
* entered after the await would not have fixed it — the enqueue itself would race.
|
|
377
|
+
*/
|
|
378
|
+
let pendingObservation: Promise<void> = Promise.resolve();
|
|
379
|
+
|
|
380
|
+
function notifyCodexQuotaSnapshot(accountId: string, next: StoredAccountQuota): void {
|
|
381
|
+
// Copy before the boundary: `next` is the live map value and the following write mutates
|
|
382
|
+
// it, so an observation that read it after awaiting could see a later snapshot than the
|
|
383
|
+
// one it was called for.
|
|
384
|
+
const snapshot = { ...next };
|
|
385
|
+
pendingObservation = pendingObservation
|
|
386
|
+
.then(async () => {
|
|
387
|
+
const observer = await import("../quota/reset-observer");
|
|
388
|
+
if (!observer.hasQuotaResetSink()) return;
|
|
389
|
+
const { codexWindowObservations } = await import("../quota/window-mapping");
|
|
390
|
+
observer.observeQuotaSnapshot({
|
|
391
|
+
scope: "codex",
|
|
392
|
+
accountKey: accountId,
|
|
393
|
+
windows: codexWindowObservations(snapshot),
|
|
394
|
+
});
|
|
395
|
+
})
|
|
396
|
+
.catch(() => {
|
|
397
|
+
// Detection is best-effort: a quota write must never fail because of it. Swallowing
|
|
398
|
+
// here also keeps the chain alive — a rejected link would poison every later one.
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Await the observation chain. Tests only: production never needs to join it. */
|
|
403
|
+
export function flushQuotaObservationsForTests(): Promise<void> {
|
|
404
|
+
return pendingObservation;
|
|
344
405
|
}
|
|
345
406
|
|
|
346
407
|
export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
@@ -366,7 +427,7 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
366
427
|
// it into weeklyPercent both discards the real weekly reading and leaves the account looking
|
|
367
428
|
// exhausted long after the burst window resets. Duration decides, exactly as parseUsageQuota
|
|
368
429
|
// already does for the WHAM payload — the two parsers must not disagree about the same data.
|
|
369
|
-
const primaryIsShort =
|
|
430
|
+
const primaryIsShort = isExplicitShortWindowMinutes(primaryWindowMinutes);
|
|
370
431
|
|
|
371
432
|
if (primaryIsMonthly) {
|
|
372
433
|
if (primaryPercent !== undefined) {
|
|
@@ -382,12 +443,10 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
382
443
|
if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
|
|
383
444
|
}
|
|
384
445
|
} else if (primaryIsShort) {
|
|
385
|
-
if (primaryPercent !== undefined)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60);
|
|
390
|
-
}
|
|
446
|
+
if (primaryPercent !== undefined) quota.shortPercent = primaryPercent;
|
|
447
|
+
if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
|
|
448
|
+
const minutes = windowMinutes_(primaryWindowMinutes);
|
|
449
|
+
if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60);
|
|
391
450
|
// The burst window vacates the primary slot, so the weekly reading is the secondary — which
|
|
392
451
|
// is where it was all along. Without this the true weekly value is silently dropped.
|
|
393
452
|
if (secondaryPercent !== undefined) {
|
|
@@ -417,10 +476,14 @@ export function applyAccountQuotaFromUpstreamHeaders(
|
|
|
417
476
|
accountId: string,
|
|
418
477
|
headers: Headers,
|
|
419
478
|
writerGeneration = captureConfigGeneration(),
|
|
479
|
+
mainWriter?: MainQuotaWriter,
|
|
420
480
|
): void {
|
|
421
481
|
const quota = parseUpstreamQuotaHeaders(headers);
|
|
422
482
|
if (!quota) return;
|
|
423
|
-
|
|
483
|
+
const policyQuota = [
|
|
484
|
+
"x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent",
|
|
485
|
+
].some(name => isInvalidPolicyUsagePercent(headers.get(name))) ? null : filterMainPolicyMonthlyQuota(quota);
|
|
486
|
+
setAccountQuotaFromParsed(accountId, quota, writerGeneration, mainWriter, policyQuota);
|
|
424
487
|
}
|
|
425
488
|
|
|
426
489
|
export function updateAccountQuota(
|
|
@@ -433,10 +496,11 @@ export function updateAccountQuota(
|
|
|
433
496
|
writerGeneration = captureConfigGeneration(),
|
|
434
497
|
): void {
|
|
435
498
|
if (!mayCommitAccountQuota(accountId, writerGeneration)) return;
|
|
436
|
-
const existing = accountQuota.get(accountId);
|
|
437
499
|
const nextWeekly = normalizeUsagePercent(weekly);
|
|
438
500
|
const nextMonthly = normalizeUsagePercent(monthly);
|
|
439
501
|
if (nextWeekly === undefined && nextMonthly === undefined && resetCredits === undefined) return;
|
|
502
|
+
hydrateAccountQuotasFromDisk();
|
|
503
|
+
const existing = accountQuota.get(accountId);
|
|
440
504
|
|
|
441
505
|
const quota: StoredAccountQuota = {
|
|
442
506
|
...(existing?.weeklyPercent !== undefined ? { weeklyPercent: existing.weeklyPercent } : {}),
|
|
@@ -449,6 +513,7 @@ export function updateAccountQuota(
|
|
|
449
513
|
...(existing?.weeklyResetAt !== undefined ? { weeklyResetAt: existing.weeklyResetAt } : {}),
|
|
450
514
|
...(existing?.monthlyResetAt !== undefined ? { monthlyResetAt: existing.monthlyResetAt } : {}),
|
|
451
515
|
...(existing?.shortPercent !== undefined ? { shortPercent: existing.shortPercent } : {}),
|
|
516
|
+
...(existing?.shortObservedAt !== undefined ? { shortObservedAt: existing.shortObservedAt } : {}),
|
|
452
517
|
...(existing?.shortResetAt !== undefined ? { shortResetAt: existing.shortResetAt } : {}),
|
|
453
518
|
...(existing?.shortWindowSeconds !== undefined ? { shortWindowSeconds: existing.shortWindowSeconds } : {}),
|
|
454
519
|
...(existing?.customWindows !== undefined ? { customWindows: existing.customWindows } : {}),
|
|
@@ -473,7 +538,40 @@ export function updateAccountQuota(
|
|
|
473
538
|
if (resetCredits !== undefined) quota.resetCredits = resetCredits;
|
|
474
539
|
|
|
475
540
|
accountQuota.set(accountId, quota);
|
|
541
|
+
// This legacy writer has no physical credential provenance.
|
|
542
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) mainPolicyQuota = null;
|
|
476
543
|
schedulePersistAccountQuotas();
|
|
544
|
+
// Observed like the other committed write. This function has no in-repo caller today, but it
|
|
545
|
+
// is re-exported as public API through src/codex/auth-api.ts, so a future caller would
|
|
546
|
+
// otherwise bypass detection AND leave a stale baseline that corrupts the next real diff.
|
|
547
|
+
// The credits-only path at setAccountQuotaFromParsed deliberately does not notify; this one
|
|
548
|
+
// writes window percentages and deadlines, so it must.
|
|
549
|
+
notifyCodexQuotaSnapshot(accountId, quota);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Bounded, known policy fields only: disk input cannot extend a DTO or retain credentials. */
|
|
553
|
+
function readMainPolicyQuota(value: unknown): MainPolicyQuota | null {
|
|
554
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
555
|
+
const entry = value as Record<string, unknown>;
|
|
556
|
+
if (typeof entry.identityKey !== "string" || !/^[a-f0-9]{64}$/.test(entry.identityKey)) return null;
|
|
557
|
+
if (!entry.quota || typeof entry.quota !== "object" || Array.isArray(entry.quota)) return null;
|
|
558
|
+
const raw = entry.quota as Record<string, unknown>;
|
|
559
|
+
if (typeof raw.updatedAt !== "number" || !Number.isFinite(raw.updatedAt) || raw.updatedAt < 0) return null;
|
|
560
|
+
const quota: StoredAccountQuota = { updatedAt: raw.updatedAt };
|
|
561
|
+
for (const field of ["weeklyPercent", "monthlyPercent", "shortPercent"] as const) {
|
|
562
|
+
const number = raw[field];
|
|
563
|
+
if (typeof number === "number" && Number.isFinite(number) && number >= 0 && number <= 100) {
|
|
564
|
+
quota[field] = number;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
for (const field of [
|
|
568
|
+
"weeklyResetAt", "monthlyResetAt", "shortResetAt", "shortObservedAt", "shortWindowSeconds", "resetCredits",
|
|
569
|
+
] as const) {
|
|
570
|
+
const number = raw[field];
|
|
571
|
+
if (typeof number === "number" && Number.isFinite(number) && number >= 0) quota[field] = number;
|
|
572
|
+
}
|
|
573
|
+
if (quota.monthlyPercent !== undefined && raw.monthlyIsPrimaryWindow === true) quota.monthlyIsPrimaryWindow = true;
|
|
574
|
+
return { identityKey: entry.identityKey, quota };
|
|
477
575
|
}
|
|
478
576
|
|
|
479
577
|
function hydrateAccountQuotasFromDisk(): void {
|
|
@@ -485,6 +583,8 @@ function hydrateAccountQuotasFromDisk(): void {
|
|
|
485
583
|
const raw = readFileSync(path, "utf8");
|
|
486
584
|
const parsed = JSON.parse(raw) as QuotaDiskFile;
|
|
487
585
|
if (!parsed || parsed.version !== 1 || !parsed.quotas || typeof parsed.quotas !== "object") return;
|
|
586
|
+
// Policy evidence deliberately outlives the legacy six-hour rotation-cache TTL.
|
|
587
|
+
mainPolicyQuota = readMainPolicyQuota(parsed.mainPolicyQuota);
|
|
488
588
|
const now = Date.now();
|
|
489
589
|
for (const [accountId, quota] of Object.entries(parsed.quotas)) {
|
|
490
590
|
if (!quota || typeof quota !== "object" || typeof quota.updatedAt !== "number") continue;
|
|
@@ -505,7 +605,11 @@ function schedulePersistAccountQuotas(): void {
|
|
|
505
605
|
for (const [accountId, quota] of accountQuota.entries()) {
|
|
506
606
|
quotas[accountId] = quota;
|
|
507
607
|
}
|
|
508
|
-
const body: QuotaDiskFile = {
|
|
608
|
+
const body: QuotaDiskFile = {
|
|
609
|
+
version: 1,
|
|
610
|
+
quotas,
|
|
611
|
+
...(mainPolicyQuota ? { mainPolicyQuota } : {}),
|
|
612
|
+
};
|
|
509
613
|
atomicWriteFile(join(getConfigDir(), QUOTA_CACHE_FILENAME), `${JSON.stringify(body)}\n`);
|
|
510
614
|
} catch {
|
|
511
615
|
// Best-effort persistence only.
|
|
@@ -518,18 +622,52 @@ export function getAccountQuota(accountId: string): StoredAccountQuota | null {
|
|
|
518
622
|
return accountQuota.get(accountId) ?? null;
|
|
519
623
|
}
|
|
520
624
|
|
|
625
|
+
/** No physical-auth reads; unrelated legacy quota consumers cannot mutate this evidence. */
|
|
626
|
+
export function getMainPolicyQuota(): StoredAccountQuota | null {
|
|
627
|
+
hydrateAccountQuotasFromDisk();
|
|
628
|
+
if (!mainPolicyQuota || mainPolicyQuota.identityKey !== getObservedMainQuotaIdentityKey()) return null;
|
|
629
|
+
return structuredClone(mainPolicyQuota.quota);
|
|
630
|
+
}
|
|
631
|
+
|
|
521
632
|
export function listAccountQuotas(): IterableIterator<[string, StoredAccountQuota]> {
|
|
522
633
|
hydrateAccountQuotasFromDisk();
|
|
523
634
|
return accountQuota.entries();
|
|
524
635
|
}
|
|
525
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Tell the observer to drop the baseline for a row that was deliberately cleared.
|
|
639
|
+
*
|
|
640
|
+
* Queued on the same chain as observations so it cannot overtake an in-flight one and be
|
|
641
|
+
* immediately re-established by it. Without this, reauth of a used account left the observer
|
|
642
|
+
* holding the pre-clear percentages and the first fresh write fired a false surprise reset
|
|
643
|
+
* (measured 91% -> 0%).
|
|
644
|
+
*/
|
|
645
|
+
function forgetCodexQuotaBaseline(accountId?: string): void {
|
|
646
|
+
pendingObservation = pendingObservation
|
|
647
|
+
.then(async () => {
|
|
648
|
+
const observer = await import("../quota/reset-observer");
|
|
649
|
+
observer.forgetQuotaBaseline({
|
|
650
|
+
scope: "codex",
|
|
651
|
+
...(accountId !== undefined ? { accountKey: accountId } : {}),
|
|
652
|
+
});
|
|
653
|
+
})
|
|
654
|
+
.catch(() => {
|
|
655
|
+
// Best-effort; a failure can only cost a re-baseline.
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
526
659
|
export function clearAccountQuota(accountId?: string): void {
|
|
527
660
|
if (accountId) {
|
|
661
|
+
hydrateAccountQuotasFromDisk();
|
|
528
662
|
accountQuota.delete(accountId);
|
|
663
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) mainPolicyQuota = null;
|
|
529
664
|
schedulePersistAccountQuotas();
|
|
665
|
+
forgetCodexQuotaBaseline(accountId);
|
|
530
666
|
return;
|
|
531
667
|
}
|
|
532
668
|
accountQuota.clear();
|
|
669
|
+
forgetCodexQuotaBaseline();
|
|
670
|
+
mainPolicyQuota = null;
|
|
533
671
|
diskHydrated = false;
|
|
534
672
|
if (persistTimer) {
|
|
535
673
|
clearTimeout(persistTimer);
|
|
@@ -558,6 +696,27 @@ export function reconcileCodexQuotaAccounts(context: GenerationContext): number
|
|
|
558
696
|
return removed;
|
|
559
697
|
}
|
|
560
698
|
|
|
699
|
+
/** Supplementary monthly bars are not governing policy evidence without plan/primary proof. */
|
|
700
|
+
function filterMainPolicyMonthlyQuota(
|
|
701
|
+
quota: Omit<StoredAccountQuota, "updatedAt"> | null,
|
|
702
|
+
monthlyOnlyPlan = false,
|
|
703
|
+
): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
704
|
+
if (!quota || monthlyOnlyPlan || quota.monthlyIsPrimaryWindow === true) return quota;
|
|
705
|
+
const filtered = { ...quota };
|
|
706
|
+
delete filtered.monthlyPercent;
|
|
707
|
+
delete filtered.monthlyResetAt;
|
|
708
|
+
delete filtered.monthlyIsPrimaryWindow;
|
|
709
|
+
// Null retains the matching prior observation; an empty object would merge away evidence.
|
|
710
|
+
return hasKnownQuotaValue(filtered) || filtered.resetCredits !== undefined ? filtered : null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Ordinary main policy rejects an entire message containing any invalid numeric window. */
|
|
714
|
+
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
715
|
+
const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
|
|
716
|
+
if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null;
|
|
717
|
+
return filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type));
|
|
718
|
+
}
|
|
719
|
+
|
|
561
720
|
export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
562
721
|
const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number"
|
|
563
722
|
? data.rate_limit_reset_credits.available_count
|
|
@@ -597,10 +756,10 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
597
756
|
const primaryIsShort = isExplicitShortWindow(primaryWindow);
|
|
598
757
|
const weeklyCandidatePercent = primaryIsShort ? undefined : primaryPercent;
|
|
599
758
|
const weeklyCandidateResetAt = primaryIsShort ? undefined : primaryResetAt;
|
|
600
|
-
//
|
|
601
|
-
// the
|
|
602
|
-
if (primaryIsShort
|
|
603
|
-
quota.shortPercent = primaryPercent;
|
|
759
|
+
// Retain the declared burst tuple even when its usage is unknown. Its shape selects
|
|
760
|
+
// the short-window policy; a missing reading is not permission to fall back to weekly.
|
|
761
|
+
if (primaryIsShort) {
|
|
762
|
+
if (primaryPercent !== undefined) quota.shortPercent = primaryPercent;
|
|
604
763
|
if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
|
|
605
764
|
const seconds = primaryWindow?.limit_window_seconds;
|
|
606
765
|
if (typeof seconds === "number" && Number.isFinite(seconds)) quota.shortWindowSeconds = seconds;
|
|
@@ -623,11 +782,11 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
623
782
|
if (!thirtyDayOnly && monthlyPercent !== undefined) {
|
|
624
783
|
quota.monthlyPercent = monthlyPercent;
|
|
625
784
|
if (monthlyResetAt !== undefined) quota.monthlyResetAt = monthlyResetAt;
|
|
626
|
-
// Record WHERE this reading came from. Only an explicitly-monthly primary window is the
|
|
627
|
-
// account's governing quota; a tertiary window lands in the same field but describes a
|
|
628
|
-
// different period, so recovery must not treat the two as interchangeable.
|
|
629
|
-
if (primaryIsMonthly && primaryPercent !== undefined) quota.monthlyIsPrimaryWindow = true;
|
|
630
785
|
}
|
|
786
|
+
// Provenance depends on the observed window, not the plan name. Go/Free need it
|
|
787
|
+
// too so a real monthly-primary replacement can retire an earlier weekly policy tuple.
|
|
788
|
+
// A supplementary value (including fallback from an unreadable primary) is not proof.
|
|
789
|
+
if (primaryIsMonthly && primaryPercent !== undefined) quota.monthlyIsPrimaryWindow = true;
|
|
631
790
|
|
|
632
791
|
const spark = data.additional_rate_limits?.find(additional => {
|
|
633
792
|
const name = String(additional.limit_name ?? "").toLowerCase();
|