@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.
Files changed (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -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
- export type StoredAccountQuota = {
8
- weeklyPercent?: number;
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 — usage percents only (no emails/tokens). */
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 existing = accountQuota.get(accountId);
281
- const next: StoredAccountQuota = { updatedAt: Date.now() };
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
- accountQuota.set(accountId, next);
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) && !snapshotHasWeekly(quota)) {
304
- // Monthly-only snapshots intentionally clear stale weekly values (issue #382).
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
- if (snapshotHasShort(quota)) {
326
- if (quota.shortPercent !== undefined) next.shortPercent = quota.shortPercent;
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
- // Header and reset-credit updates are partial snapshots. Preserve the last full WHAM
331
- // burst tuple when those updates do not carry enough window metadata to replace it.
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
- accountQuota.set(accountId, next);
343
- schedulePersistAccountQuotas();
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 = primaryRaw !== null && isExplicitShortWindowMinutes(primaryWindowMinutes);
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
- quota.shortPercent = primaryPercent;
387
- if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
388
- const minutes = windowMinutes_(primaryWindowMinutes);
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
- setAccountQuotaFromParsed(accountId, quota, writerGeneration);
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 = { version: 1, quotas };
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
- // Keep the burst reading instead of dropping it on the floor: it is a real limit, and
601
- // the account is blocked when it fills even though the weekly window is fine (#1791).
602
- if (primaryIsShort && primaryPercent !== undefined) {
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();