@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-D_t6sCWs.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -0,0 +1,98 @@
1
+ import type { QuotaHistorySample, QuotaHistoryWindow } from "./quota-history";
2
+ import type { PersistedUsageAttempt, PersistedUsageEntry } from "../usage/log";
3
+
4
+ export const CAPACITY_ASSUMPTIONS = [
5
+ "Quota percentages can be rounded or delayed.",
6
+ "Only retained valid proxy usage rows are observed; external usage is unknown.",
7
+ "Account log labels are assumed stable within each observation interval.",
8
+ "This low-confidence effective-token estimate is not a provider token limit or lower bound.",
9
+ ] as const;
10
+ export const CAPACITY_REASONS = ["insufficient_intervals", "ledger_unavailable", "ledger_truncated", "identity_unavailable", "identity_changed", "ambiguous_usage"] as const;
11
+ export type CapacityReason = typeof CAPACITY_REASONS[number];
12
+ export function parseCapacityReason(value: unknown): CapacityReason | undefined {
13
+ return CAPACITY_REASONS.find(reason => reason === value);
14
+ }
15
+ export interface CodexCapacityResult {
16
+ status: "estimated" | "insufficient-evidence";
17
+ estimates: Array<{ window: QuotaHistoryWindow["window"]; estimatedTokens: number; sampleCount: number; confidence: "low" }>;
18
+ reason?: CapacityReason;
19
+ assumptions: readonly string[];
20
+ }
21
+ export function insufficientCodexCapacity(reason: CapacityReason): CodexCapacityResult {
22
+ return { status: "insufficient-evidence", estimates: [], reason, assumptions: [...CAPACITY_ASSUMPTIONS] };
23
+ }
24
+ const nonnegative = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
25
+
26
+ function reportedTokens(attempt: PersistedUsageAttempt): number | undefined {
27
+ if (attempt.sendCount !== 1 || attempt.usageStatus !== "reported" || attempt.locallyAnswered === true
28
+ || !attempt.usage || attempt.usage.estimated === true
29
+ || !nonnegative(attempt.usage.inputTokens) || !nonnegative(attempt.usage.outputTokens)) return undefined;
30
+ const total = attempt.usage.totalTokens ?? attempt.usage.inputTokens + attempt.usage.outputTokens;
31
+ return nonnegative(total) ? total : undefined;
32
+ }
33
+
34
+ /** Informational inference over raw same-window observations, never an account-selection input. */
35
+ export function estimateCodexQuotaCapacity(
36
+ observations: ReadonlyArray<Omit<QuotaHistorySample, "credentialGeneration">>,
37
+ entries: readonly PersistedUsageEntry[],
38
+ label: string,
39
+ sharedQuotaModel: (model: string) => boolean,
40
+ ): CodexCapacityResult {
41
+ if (entries.length > 10_000) return insufficientCodexCapacity("ledger_truncated");
42
+ const requests = new Map<string, PersistedUsageEntry>();
43
+ for (const entry of entries) {
44
+ const previous = requests.get(entry.requestId);
45
+ if (previous && JSON.stringify(previous) !== JSON.stringify(entry)) return insufficientCodexCapacity("ambiguous_usage");
46
+ requests.set(entry.requestId, entry);
47
+ }
48
+ const sorted = [...observations].sort((a, b) => a.observedAt - b.observedAt);
49
+ const estimates: CodexCapacityResult["estimates"] = [];
50
+ for (const windowName of ["short", "weekly", "monthly"] as const) {
51
+ const points = sorted.flatMap(row => {
52
+ const window = row.windows.find(candidate => candidate.family === "account" && candidate.window === windowName);
53
+ return window ? [{ ...window, at: row.observedAt, source: row.source }] : [];
54
+ });
55
+ const samples: number[] = [];
56
+ for (let index = 1; index < points.length; index++) {
57
+ const left = points[index - 1], right = points[index];
58
+ const delta = right.usedPercent - left.usedPercent;
59
+ if (left.source !== right.source || !nonnegative(left.resetAtMs) || left.resetAtMs !== right.resetAtMs
60
+ || left.resetAtMs <= right.at || left.windowSeconds !== right.windowSeconds
61
+ || left.monthlyIsPrimaryWindow !== right.monthlyIsPrimaryWindow
62
+ || right.at <= left.at || delta < 1 || delta > 100 || !Number.isFinite(delta)) continue;
63
+ let tokens = 0;
64
+ let valid = true;
65
+ for (const entry of requests.values()) {
66
+ if (!nonnegative(entry.timestamp) || !nonnegative(entry.durationMs)) continue;
67
+ const end = entry.timestamp + entry.durationMs;
68
+ if (!Number.isFinite(end) || entry.timestamp <= left.at || end > right.at) continue;
69
+ // Untimed or absent physical-attempt evidence cannot be reconstructed from parent totals.
70
+ if (!entry.attempts?.length) continue;
71
+ const attempts = new Map<number, PersistedUsageAttempt>();
72
+ for (const attempt of entry.attempts) {
73
+ const prior = attempts.get(attempt.ordinal);
74
+ if (prior && JSON.stringify(prior) !== JSON.stringify(attempt)) { valid = false; break; }
75
+ attempts.set(attempt.ordinal, attempt);
76
+ }
77
+ if (!valid) break;
78
+ for (const attempt of attempts.values()) {
79
+ if (attempt.accountLogLabel !== label || attempt.adapter !== "openai-responses" || !sharedQuotaModel(attempt.model)) continue;
80
+ const reported = reportedTokens(attempt);
81
+ if (reported === undefined) continue;
82
+ tokens += reported;
83
+ }
84
+ }
85
+ const inferred = tokens * 100 / delta;
86
+ if (valid && tokens > 0 && Number.isFinite(inferred) && inferred > 0) samples.push(inferred);
87
+ }
88
+ if (samples.length) {
89
+ samples.sort((a, b) => a - b);
90
+ const middle = Math.floor(samples.length / 2);
91
+ const median = samples.length % 2 ? samples[middle] : samples[middle - 1] / 2 + samples[middle] / 2;
92
+ const estimatedTokens = Math.round(median);
93
+ if (estimatedTokens > 0) estimates.push({ window: windowName, estimatedTokens, sampleCount: samples.length, confidence: "low" });
94
+ }
95
+ }
96
+ return estimates.length ? { status: "estimated", estimates, assumptions: [...CAPACITY_ASSUMPTIONS] }
97
+ : insufficientCodexCapacity("insufficient_intervals");
98
+ }
@@ -0,0 +1,160 @@
1
+ import { isValidCodexAccountId } from "./account-id";
2
+ import type { PoolQuotaWriter } from "./quota-types";
3
+
4
+ export const QUOTA_HISTORY_LIMITS = { perAccount: 200, accounts: 64, samples: 4096, bytes: 2 * 1024 * 1024, ageMs: 30 * 86400_000 } as const;
5
+ export interface QuotaHistoryWindow {
6
+ family: "account" | "spark";
7
+ window: "short" | "weekly" | "monthly";
8
+ usedPercent: number;
9
+ resetAtMs?: number;
10
+ windowSeconds?: number;
11
+ monthlyIsPrimaryWindow?: boolean;
12
+ }
13
+ export interface QuotaHistorySample {
14
+ observedAt: number;
15
+ source: "wham" | "response-header";
16
+ credentialGeneration: number;
17
+ windows: QuotaHistoryWindow[];
18
+ }
19
+ type Envelope = { identity: string; samples: QuotaHistorySample[] };
20
+ type Bucket = Envelope & { costs: number[]; overhead: number };
21
+ const encoder = new TextEncoder();
22
+ const byteSize = (value: unknown) => encoder.encode(JSON.stringify(value)).byteLength;
23
+ const identityPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
24
+ const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
25
+ const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
26
+
27
+ /** Reconstruct allowlisted data at the disk boundary; malformed windows cannot be partial evidence. */
28
+ function parseSample(value: unknown, now: number): QuotaHistorySample | undefined {
29
+ if (!record(value) || !finite(value.observedAt) || value.observedAt > now
30
+ || value.observedAt < now - QUOTA_HISTORY_LIMITS.ageMs
31
+ || !Number.isSafeInteger(value.credentialGeneration) || (value.credentialGeneration as number) < 0
32
+ || (value.source !== "wham" && value.source !== "response-header")
33
+ || !Array.isArray(value.windows) || !value.windows.length || value.windows.length > 5) return undefined;
34
+ const windows: QuotaHistoryWindow[] = [];
35
+ const seen = new Set<string>();
36
+ for (const item of value.windows) {
37
+ if (!record(item) || (item.family !== "account" && item.family !== "spark")
38
+ || (item.window !== "short" && item.window !== "weekly" && item.window !== "monthly")
39
+ || (item.family === "spark" && item.window === "monthly")
40
+ || !finite(item.usedPercent) || item.usedPercent > 100) return undefined;
41
+ const key = `${item.family}:${item.window}`;
42
+ if (seen.has(key)) return undefined;
43
+ seen.add(key);
44
+ if ((item.resetAtMs !== undefined && !finite(item.resetAtMs))
45
+ || (item.windowSeconds !== undefined && (!finite(item.windowSeconds) || item.windowSeconds === 0))
46
+ || (item.monthlyIsPrimaryWindow !== undefined && typeof item.monthlyIsPrimaryWindow !== "boolean")) return undefined;
47
+ windows.push({ family: item.family, window: item.window, usedPercent: item.usedPercent,
48
+ ...(item.resetAtMs !== undefined ? { resetAtMs: item.resetAtMs as number } : {}),
49
+ ...(item.windowSeconds !== undefined ? { windowSeconds: item.windowSeconds as number } : {}),
50
+ ...(item.monthlyIsPrimaryWindow === true && item.family === "account" && item.window === "monthly" ? { monthlyIsPrimaryWindow: true } : {}),
51
+ });
52
+ }
53
+ return { observedAt: value.observedAt, source: value.source, credentialGeneration: value.credentialGeneration as number, windows };
54
+ }
55
+
56
+ /** Bounded in-process observations. The quota cache owns persistence and credential admission. */
57
+ export class CodexQuotaHistory {
58
+ private accounts = new Map<string, Bucket>();
59
+ private bytes = 32;
60
+ private count = 0;
61
+
62
+ append(writer: PoolQuotaWriter, sample: QuotaHistorySample, now = Date.now()): void {
63
+ if (!isValidCodexAccountId(writer.accountId) || !identityPattern.test(writer.historyIdentity)
64
+ || sample.credentialGeneration !== writer.credentialGeneration) return;
65
+ const parsed = parseSample(sample, now);
66
+ if (!parsed) return;
67
+ let bucket = this.accounts.get(writer.accountId);
68
+ if (bucket && bucket.identity !== writer.historyIdentity) { this.clear(writer.accountId); bucket = undefined; }
69
+ if (!bucket) {
70
+ const overhead = byteSize(writer.accountId) + byteSize({ identity: writer.historyIdentity, samples: [] }) + 8;
71
+ bucket = { identity: writer.historyIdentity, samples: [], costs: [], overhead };
72
+ this.accounts.set(writer.accountId, bucket);
73
+ this.bytes += overhead;
74
+ }
75
+ const index = bucket.samples.findIndex(row => row.observedAt > parsed.observedAt);
76
+ const position = index < 0 ? bucket.samples.length : index;
77
+ const cost = byteSize(parsed) + 1;
78
+ bucket.samples.splice(position, 0, parsed);
79
+ bucket.costs.splice(position, 0, cost);
80
+ this.bytes += cost;
81
+ this.count++;
82
+ while (bucket.samples.length > QUOTA_HISTORY_LIMITS.perAccount) this.dropFirst(writer.accountId);
83
+ this.prune(now);
84
+ }
85
+
86
+ read(accountId: string, identity: string | undefined, now = Date.now(), limit: number = QUOTA_HISTORY_LIMITS.perAccount): { samples: QuotaHistorySample[]; truncated: boolean } {
87
+ this.prune(now);
88
+ const bucket = this.accounts.get(accountId);
89
+ if (!identity || !bucket) return { samples: [], truncated: false };
90
+ if (bucket.identity !== identity) { this.clear(accountId); return { samples: [], truncated: false }; }
91
+ const capped = Math.max(1, Math.min(QUOTA_HISTORY_LIMITS.perAccount, Math.trunc(limit)));
92
+ return { samples: structuredClone(bucket.samples.slice(-capped)), truncated: bucket.samples.length > capped };
93
+ }
94
+
95
+ clear(accountId?: string): number {
96
+ if (accountId === undefined) {
97
+ const count = this.accounts.size;
98
+ this.accounts.clear(); this.count = 0; this.bytes = 32;
99
+ return count;
100
+ }
101
+ const bucket = this.accounts.get(accountId);
102
+ if (!bucket) return 0;
103
+ this.bytes -= bucket.overhead + bucket.costs.reduce((a, b) => a + b, 0);
104
+ this.count -= bucket.samples.length;
105
+ this.accounts.delete(accountId);
106
+ return 1;
107
+ }
108
+
109
+ reconcile(ids: ReadonlySet<string>): number {
110
+ let removed = 0;
111
+ for (const id of this.accounts.keys()) if (!ids.has(id)) removed += this.clear(id);
112
+ return removed;
113
+ }
114
+
115
+ serialize(now = Date.now()): { version: 1; accounts: Record<string, Envelope> } {
116
+ this.prune(now);
117
+ return { version: 1, accounts: Object.fromEntries([...this.accounts].map(([id, bucket]) => [id,
118
+ { identity: bucket.identity, samples: structuredClone(bucket.samples) }])) };
119
+ }
120
+
121
+ hydrate(value: unknown, now = Date.now()): void {
122
+ this.clear();
123
+ if (!record(value) || value.version !== 1 || !record(value.accounts) || byteSize(value) > QUOTA_HISTORY_LIMITS.bytes) return;
124
+ const entries = Object.entries(value.accounts);
125
+ if (entries.length > QUOTA_HISTORY_LIMITS.accounts) return;
126
+ let count = 0;
127
+ for (const [id, envelope] of entries) {
128
+ if (!isValidCodexAccountId(id) || !record(envelope) || typeof envelope.identity !== "string"
129
+ || !identityPattern.test(envelope.identity) || !Array.isArray(envelope.samples)
130
+ || envelope.samples.length > QUOTA_HISTORY_LIMITS.perAccount) return;
131
+ count += envelope.samples.length;
132
+ if (count > QUOTA_HISTORY_LIMITS.samples) return;
133
+ }
134
+ for (const [accountId, raw] of entries) {
135
+ const envelope = raw as Envelope;
136
+ for (const sample of envelope.samples) {
137
+ const parsed = parseSample(sample, now);
138
+ if (parsed) this.append({ accountId, historyIdentity: envelope.identity, credentialGeneration: parsed.credentialGeneration }, parsed, now);
139
+ }
140
+ }
141
+ }
142
+
143
+ private dropFirst(id: string): void {
144
+ const bucket = this.accounts.get(id)!;
145
+ this.bytes -= bucket.costs.shift()!;
146
+ bucket.samples.shift(); this.count--;
147
+ if (!bucket.samples.length) { this.bytes -= bucket.overhead; this.accounts.delete(id); }
148
+ }
149
+
150
+ private prune(now: number): void {
151
+ for (const [id, bucket] of this.accounts) {
152
+ while (bucket.samples.length && bucket.samples[0].observedAt < now - QUOTA_HISTORY_LIMITS.ageMs) this.dropFirst(id);
153
+ }
154
+ while (this.accounts.size > QUOTA_HISTORY_LIMITS.accounts || this.count > QUOTA_HISTORY_LIMITS.samples || this.bytes > QUOTA_HISTORY_LIMITS.bytes) {
155
+ const first = [...this.accounts].sort(([a, x], [b, y]) => x.samples[0].observedAt - y.samples[0].observedAt || a.localeCompare(b))[0];
156
+ if (!first) break;
157
+ this.dropFirst(first[0]);
158
+ }
159
+ }
160
+ }
@@ -49,3 +49,11 @@ export type WhamUsageResponse = {
49
49
  rate_limit_reset_credits?: { available_count: number } | null;
50
50
  additional_rate_limits?: WhamAdditionalRateLimit[] | null;
51
51
  };
52
+
53
+
54
+ /** Captured from the exact dispatched pool credential; never a management API field. */
55
+ export interface PoolQuotaWriter {
56
+ accountId: string;
57
+ credentialGeneration: number;
58
+ historyIdentity: string;
59
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
1
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, openSync, readSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { atomicWriteFile, getConfigDir } from "../config";
4
4
  import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
@@ -6,10 +6,12 @@ import { isThirtyDayOnlyCodexPlan } from "./plan";
6
6
  import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
7
7
  import { getObservedMainQuotaIdentityKey, isMainQuotaWriterLive, type MainQuotaWriter } from "./main-account-cache";
8
8
 
9
- import type { StoredAccountQuota, WhamUsageResponse, WhamUsageWindow } from "./quota-types";
9
+ import { CodexQuotaHistory, QUOTA_HISTORY_LIMITS, type QuotaHistoryWindow } from "./quota-history";
10
+ import { isPoolQuotaWriterLive, poolQuotaHistoryIdentity } from "./account-store";
11
+ import type { PoolQuotaWriter, StoredAccountQuota, WhamUsageResponse, WhamUsageWindow } from "./quota-types";
10
12
  export type { StoredAccountQuota, WhamUsageResponse } from "./quota-types";
11
13
 
12
- /** Disk snapshot under OPENCODEX_HOME — quota and policy identity only, never credential tags. */
14
+ /** Disk snapshot: quota, private non-secret publication UUIDs and policy identity; never token-derived fingerprints. */
13
15
  const QUOTA_CACHE_FILENAME = "codex-quota-cache.json";
14
16
  /** Keep last-known bars across restarts; WHAM still refreshes on TTL in live/prime paths. */
15
17
  const QUOTA_DISK_MAX_AGE_MS = 6 * 60 * 60_000;
@@ -19,6 +21,7 @@ type QuotaDiskFile = {
19
21
  version: 1;
20
22
  quotas: Record<string, StoredAccountQuota>;
21
23
  mainPolicyQuota?: MainPolicyQuota;
24
+ history?: ReturnType<CodexQuotaHistory["serialize"]>;
22
25
  };
23
26
 
24
27
  type MainPolicyQuota = { identityKey: string; quota: StoredAccountQuota };
@@ -61,6 +64,7 @@ export function resetAtToMs(resetAt: number): number {
61
64
  }
62
65
 
63
66
  const accountQuota = new Map<string, StoredAccountQuota>();
67
+ const quotaHistory = new CodexQuotaHistory();
64
68
  let lastReconciledGeneration = 0;
65
69
  let liveAccountIds = new Set<string>();
66
70
 
@@ -268,14 +272,22 @@ export function setAccountQuotaFromParsed(
268
272
  writerGeneration = captureConfigGeneration(),
269
273
  mainWriter?: MainQuotaWriter,
270
274
  policyQuota: Omit<StoredAccountQuota, "updatedAt"> | null = quota,
275
+ historyEvidence?: QuotaObservationEvidence,
271
276
  ): void {
272
- if (!quota) return;
277
+ quota = withoutRetiredCodexQuota(quota);
278
+ policyQuota = withoutRetiredCodexQuota(policyQuota);
279
+ if (!quota || (!snapshotHasUsage(quota) && quota.resetCredits === undefined)) return;
273
280
  if (!mayCommitAccountQuota(accountId, writerGeneration)) return;
274
281
  const isMain = accountId === MAIN_CODEX_ACCOUNT_ID;
275
282
  if (isMain && mainWriter && !isMainQuotaWriterLive(mainWriter)) return;
276
283
  hydrateAccountQuotasFromDisk();
277
284
  const legacyExisting = accountQuota.get(accountId);
278
285
  const updatedAt = Date.now();
286
+ if (historyEvidence && historyEvidence.writer.accountId === accountId && isPoolQuotaWriterLive(historyEvidence.writer)) {
287
+ quotaHistory.append(historyEvidence.writer, { observedAt: historyEvidence.observedAt, source: historyEvidence.source,
288
+ credentialGeneration: historyEvidence.writer.credentialGeneration, windows: historyWindows(historyEvidence.raw),
289
+ }, updatedAt);
290
+ }
279
291
  // Legacy rotation keeps its existing carry behavior, but never inherits policy-only
280
292
  // evidence that outlived its disk TTL. Policy has a separate, identity-checked base.
281
293
  const next = mergeAccountQuota(quota, legacyExisting, updatedAt);
@@ -307,6 +319,8 @@ function mergeAccountQuota(
307
319
  updatedAt: number,
308
320
  policyEvidence = false,
309
321
  ): StoredAccountQuota {
322
+ quota = withoutRetiredCodexQuota(quota) ?? {};
323
+ existing = withoutRetiredCodexQuota(existing ?? null) ?? undefined;
310
324
  const next: StoredAccountQuota = { updatedAt };
311
325
  const creditsOnly = quota.resetCredits !== undefined && !snapshotHasUsage(quota);
312
326
 
@@ -415,7 +429,9 @@ function notifyCodexQuotaSnapshot(accountId: string, next: StoredAccountQuota):
415
429
  // Copy before the boundary: `next` is the live map value and the following write mutates
416
430
  // it, so an observation that read it after awaiting could see a later snapshot than the
417
431
  // one it was called for.
418
- const snapshot = { ...next };
432
+ const retained = withoutRetiredCodexQuota(next);
433
+ if (!retained) return;
434
+ const snapshot = { ...retained };
419
435
  pendingObservation = pendingObservation
420
436
  .then(async () => {
421
437
  const observer = await import("../quota/reset-observer");
@@ -439,20 +455,25 @@ export function flushQuotaObservationsForTests(): Promise<void> {
439
455
  return pendingObservation;
440
456
  }
441
457
 
442
- /** Wire marker shared by Spark-family models, whose upstream limit family is model-specific. */
443
- const SPARK_MODEL_MARKER = "codex-spark";
444
- /**
445
- * Custom-window label for the Spark 5h window. The WHAM parser and the response-header path
446
- * must write the SAME label so a header refresh replaces the WHAM reading instead of doubling it.
447
- */
448
- const SPARK_SHORT_WINDOW_LABEL = "GPT-5.3-Codex-Spark 5h";
458
+ /** Exact retired model evidence, including account/provider-qualified selectors. */
459
+ export function isRetiredCodexSparkModel(modelId: string | undefined): boolean {
460
+ return modelId?.trim().toLowerCase().split("/").at(-1) === "gpt-5.3-codex-spark";
461
+ }
449
462
 
450
- /** True when the routed model belongs to the Spark family, which carries its own rate limit. */
451
- function isCodexSparkModel(modelId: string | undefined): boolean {
452
- return typeof modelId === "string" && modelId.includes(SPARK_MODEL_MARKER);
463
+ /** Tombstone old cache/DTO labels without changing unrelated custom windows or stores. */
464
+ export function withoutRetiredCodexQuota<T extends Omit<StoredAccountQuota, "updatedAt"> | null>(quota: T): T | null {
465
+ if (!quota?.customWindows?.length) return quota;
466
+ const kept = quota.customWindows.filter(window =>
467
+ window.label !== "GPT-5.3-Codex-Spark 5h" && window.label !== "GPT-5.3-Codex-Spark Weekly");
468
+ if (kept.length === quota.customWindows.length) return quota;
469
+ const next = { ...quota };
470
+ if (kept.length > 0) next.customWindows = kept;
471
+ else delete next.customWindows;
472
+ return snapshotHasUsage(next) || next.resetCredits !== undefined ? next as T : null;
453
473
  }
454
474
 
455
475
  export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?: string }): Omit<StoredAccountQuota, "updatedAt"> | null {
476
+ if (isRetiredCodexSparkModel(options?.modelId)) return null;
456
477
  const primaryRaw = headers.get("x-codex-primary-used-percent");
457
478
  const secondaryRaw = headers.get("x-codex-secondary-used-percent");
458
479
  const tertiaryRaw = headers.get("x-codex-tertiary-used-percent");
@@ -475,10 +496,6 @@ export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?
475
496
  // it into weeklyPercent both discards the real weekly reading and leaves the account looking
476
497
  // exhausted long after the burst window resets. Duration decides, exactly as parseUsageQuota
477
498
  // already does for the WHAM payload — the two parsers must not disagree about the same data.
478
- // One more attribution layer (#4122): on a Spark-family model response the sub-day primary is
479
- // the MODEL-SPECIFIC limit, not an account window. Filing it as the account short tuple made
480
- // one pool account display a 5h bar its identically-limited peers did not have, and fed a
481
- // model limit to the account-policy readers (main-account hard lock, five-hour auto-refresh).
482
499
  const primaryIsShort = isExplicitShortWindowMinutes(primaryWindowMinutes);
483
500
 
484
501
  if (primaryIsMonthly) {
@@ -495,21 +512,10 @@ export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?
495
512
  if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
496
513
  }
497
514
  } else if (primaryIsShort) {
498
- if (isCodexSparkModel(options?.modelId)) {
499
- if (primaryPercent !== undefined) {
500
- const sparkWindow: { label: string; percent: number; resetAt?: number } = {
501
- label: SPARK_SHORT_WINDOW_LABEL,
502
- percent: primaryPercent,
503
- };
504
- if (primaryResetAt !== undefined) sparkWindow.resetAt = primaryResetAt;
505
- quota.customWindows = [sparkWindow];
506
- }
507
- } else {
508
- if (primaryPercent !== undefined) quota.shortPercent = primaryPercent;
509
- if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
510
- const minutes = windowMinutes_(primaryWindowMinutes);
511
- if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60);
512
- }
515
+ if (primaryPercent !== undefined) quota.shortPercent = primaryPercent;
516
+ if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
517
+ const minutes = windowMinutes_(primaryWindowMinutes);
518
+ if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60);
513
519
  // The burst window vacates the primary slot, so the weekly reading is the secondary — which
514
520
  // is where it was all along. Without this the true weekly value is silently dropped.
515
521
  if (secondaryPercent !== undefined) {
@@ -540,31 +546,17 @@ export function applyAccountQuotaFromUpstreamHeaders(
540
546
  headers: Headers,
541
547
  writerGeneration = captureConfigGeneration(),
542
548
  mainWriter?: MainQuotaWriter,
543
- options?: { modelId?: string },
549
+ options?: { modelId?: string; poolWriter?: PoolQuotaWriter },
544
550
  ): void {
545
551
  const quota = parseUpstreamQuotaHeaders(headers, options);
546
552
  if (!quota) return;
547
553
  const policyQuota = [
548
554
  "x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent",
549
555
  ].some(name => isInvalidPolicyUsagePercent(headers.get(name))) ? null : filterMainPolicyMonthlyQuota(quota);
550
- // A header-observed Spark window is a partial update against the WHAM-recorded custom windows:
551
- // merge by label so the weekly Spark entry survives, and hydrate first so the first call in a
552
- // process does not merge against an empty map. The merged list goes only to the legacy
553
- // snapshot — the identity-bound policy evidence keeps exactly what this response said.
554
- let legacyQuota = quota;
555
- if (quota.customWindows !== undefined) {
556
- hydrateAccountQuotasFromDisk();
557
- const existing = accountQuota.get(accountId)?.customWindows;
558
- if (existing !== undefined) {
559
- const incoming = new Map(quota.customWindows.map(window => [window.label, window]));
560
- const merged = existing.map(window => incoming.get(window.label) ?? window);
561
- for (const window of quota.customWindows) {
562
- if (!existing.some(entry => entry.label === window.label)) merged.push(window);
563
- }
564
- legacyQuota = { ...quota, customWindows: merged };
565
- }
566
- }
567
- setAccountQuotaFromParsed(accountId, legacyQuota, writerGeneration, mainWriter, policyQuota);
556
+ const validHistory = !["x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent"]
557
+ .some(name => isInvalidPolicyUsagePercent(headers.get(name)));
558
+ setAccountQuotaFromParsed(accountId, quota, writerGeneration, mainWriter, policyQuota,
559
+ options?.poolWriter && validHistory ? { writer: options.poolWriter, observedAt: Date.now(), source: "response-header", raw: quota } : undefined);
568
560
  }
569
561
 
570
562
  export function updateAccountQuota(
@@ -658,16 +650,19 @@ function hydrateAccountQuotasFromDisk(): void {
658
650
  try {
659
651
  const path = join(getConfigDir(), QUOTA_CACHE_FILENAME);
660
652
  if (!existsSync(path)) return;
661
- const raw = readFileSync(path, "utf8");
653
+ const raw = readQuotaCacheBounded(path);
662
654
  const parsed = JSON.parse(raw) as QuotaDiskFile;
663
655
  if (!parsed || parsed.version !== 1 || !parsed.quotas || typeof parsed.quotas !== "object") return;
656
+ quotaHistory.hydrate(parsed.history);
664
657
  // Policy evidence deliberately outlives the legacy six-hour rotation-cache TTL.
665
658
  mainPolicyQuota = readMainPolicyQuota(parsed.mainPolicyQuota);
666
659
  const now = Date.now();
667
660
  for (const [accountId, quota] of Object.entries(parsed.quotas)) {
668
661
  if (!quota || typeof quota !== "object" || typeof quota.updatedAt !== "number") continue;
669
662
  if (now - quota.updatedAt > QUOTA_DISK_MAX_AGE_MS) continue;
670
- if (!accountQuota.has(accountId)) accountQuota.set(accountId, quota);
663
+ const retained = withoutRetiredCodexQuota(quota);
664
+ if (!retained) continue;
665
+ if (!accountQuota.has(accountId)) accountQuota.set(accountId, retained);
671
666
  }
672
667
  } catch {
673
668
  // Corrupt/missing cache must never block routing or the dashboard.
@@ -687,6 +682,7 @@ function schedulePersistAccountQuotas(): void {
687
682
  version: 1,
688
683
  quotas,
689
684
  ...(mainPolicyQuota ? { mainPolicyQuota } : {}),
685
+ history: quotaHistory.serialize(),
690
686
  };
691
687
  atomicWriteFile(join(getConfigDir(), QUOTA_CACHE_FILENAME), `${JSON.stringify(body)}\n`);
692
688
  } catch {
@@ -735,6 +731,8 @@ function forgetCodexQuotaBaseline(accountId?: string): void {
735
731
  }
736
732
 
737
733
  export function clearAccountQuota(accountId?: string): void {
734
+ if (accountId) hydrateAccountQuotasFromDisk();
735
+ quotaHistory.clear(accountId);
738
736
  if (accountId) {
739
737
  hydrateAccountQuotasFromDisk();
740
738
  accountQuota.delete(accountId);
@@ -762,7 +760,7 @@ export function clearAccountQuota(accountId?: string): void {
762
760
  export function reconcileCodexQuotaAccounts(context: GenerationContext): number {
763
761
  if (context.generation <= lastReconciledGeneration) return 0;
764
762
  hydrateAccountQuotasFromDisk();
765
- let removed = 0;
763
+ let removed = quotaHistory.reconcile(context.codexAccountIds);
766
764
  for (const accountId of accountQuota.keys()) {
767
765
  if (context.codexAccountIds.has(accountId)) continue;
768
766
  accountQuota.delete(accountId);
@@ -801,9 +799,6 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
801
799
  : undefined;
802
800
 
803
801
  if (!data.rate_limit) {
804
- if (data.additional_rate_limits?.length) {
805
- return parseUsageQuota({ ...data, rate_limit: {} });
806
- }
807
802
  return resetCredits !== undefined ? { resetCredits } : null;
808
803
  }
809
804
 
@@ -866,39 +861,71 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
866
861
  // A supplementary value (including fallback from an unreadable primary) is not proof.
867
862
  if (primaryIsMonthly && primaryPercent !== undefined) quota.monthlyIsPrimaryWindow = true;
868
863
 
869
- const spark = data.additional_rate_limits?.find(additional => {
870
- const name = String(additional.limit_name ?? "").toLowerCase();
871
- const feature = String(additional.metered_feature ?? "").toLowerCase();
872
- return feature === "codex_bengalfox" || name.includes("gpt-5.3-codex-spark");
873
- });
874
- const sparkWindows = [spark?.rate_limit?.primary_window, spark?.rate_limit?.secondary_window]
875
- .filter((window): window is WhamUsageWindow => !!window);
876
- const sparkShort = sparkWindows.find(window => {
877
- const percent = normalizeUsagePercent(window.used_percent);
878
- return percent !== undefined && isExplicitShortWindow(window);
879
- });
880
- const sparkWeekly = sparkWindows.find(window => {
881
- const percent = normalizeUsagePercent(window.used_percent);
882
- const seconds = window.limit_window_seconds;
883
- return percent !== undefined
884
- && !isExplicitShortWindow(window)
885
- && !isExplicitMonthlyWindow(window)
886
- && (seconds === undefined || seconds >= WEEKLY_WINDOW_MIN_SECONDS);
887
- });
888
- const sparkCustomWindows: Array<{ label: string; percent: number; resetAt?: number }> = [];
889
- for (const [label, window] of [
890
- [SPARK_SHORT_WINDOW_LABEL, sparkShort],
891
- ["GPT-5.3-Codex-Spark Weekly", sparkWeekly],
892
- ] as const) {
893
- const percent = normalizeUsagePercent(window?.used_percent);
894
- if (percent === undefined) continue;
895
- const sparkWindow: { label: string; percent: number; resetAt?: number } = { label, percent };
896
- const resetAt = normalizeResetAt(window?.reset_at);
897
- if (resetAt !== undefined) sparkWindow.resetAt = resetAt;
898
- sparkCustomWindows.push(sparkWindow);
899
- }
900
- if (sparkCustomWindows.length > 0) quota.customWindows = sparkCustomWindows;
901
864
  if (resetCredits !== undefined) quota.resetCredits = resetCredits;
902
865
 
903
866
  return hasKnownQuotaValue(quota) || resetCredits !== undefined ? quota : null;
904
867
  }
868
+
869
+
870
+ export interface QuotaObservationEvidence {
871
+ writer: PoolQuotaWriter;
872
+ observedAt: number;
873
+ source: "wham" | "response-header";
874
+ raw: Omit<StoredAccountQuota, "updatedAt">;
875
+ }
876
+
877
+ /** Reject raw invalid readings before the compatibility parser clamps them into valid-looking bars. */
878
+ export function isValidWhamHistoryObservation(data: WhamUsageResponse): boolean {
879
+ const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
880
+ for (const limit of Array.isArray(data.additional_rate_limits) ? data.additional_rate_limits : []) {
881
+ if (limit && typeof limit === "object") windows.push(limit.rate_limit?.primary_window, limit.rate_limit?.secondary_window);
882
+ }
883
+ return !windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent));
884
+ }
885
+
886
+ function historyWindows(quota: Omit<StoredAccountQuota, "updatedAt">): QuotaHistoryWindow[] {
887
+ const windows: QuotaHistoryWindow[] = [];
888
+ for (const window of ["short", "weekly", "monthly"] as const) {
889
+ const percent = quota[`${window}Percent`];
890
+ const reset = quota[`${window}ResetAt`];
891
+ if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100) continue;
892
+ windows.push({ family: "account", window, usedPercent: percent,
893
+ ...(typeof reset === "number" && Number.isFinite(reset) && reset >= 0 ? { resetAtMs: resetAtToMs(reset) } : {}),
894
+ ...(window === "short" && quota.shortWindowSeconds ? { windowSeconds: quota.shortWindowSeconds } : {}),
895
+ ...(window === "monthly" && quota.monthlyIsPrimaryWindow ? { monthlyIsPrimaryWindow: true } : {}),
896
+ });
897
+ }
898
+ // No observation path emits retired Spark custom windows any more: the header parser refuses the
899
+ // retired model and the WHAM parser no longer derives its family, so recording one here could only
900
+ // resurrect evidence `withoutRetiredCodexQuota` tombstones on the way in. The `spark` family stays
901
+ // in the history schema so samples already persisted by older builds still parse.
902
+ return windows;
903
+ }
904
+
905
+ /** A cache read is bounded even if a file grows between stat and read. */
906
+ function readQuotaCacheBounded(path: string): string {
907
+ const limit = 4 * 1024 * 1024;
908
+ const flags = fsConstants.O_RDONLY | (process.platform === "win32" ? 0 : fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW);
909
+ const fd = openSync(path, flags);
910
+ try {
911
+ const stat = fstatSync(fd);
912
+ if (!stat.isFile() || stat.size > limit) throw new Error("quota cache exceeds bounds");
913
+ const chunks: Buffer[] = [];
914
+ let total = 0;
915
+ while (total <= limit) {
916
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, limit + 1 - total));
917
+ const size = readSync(fd, chunk, 0, chunk.length, null);
918
+ if (!size) return Buffer.concat(chunks, total).toString("utf8");
919
+ chunks.push(chunk.subarray(0, size)); total += size;
920
+ }
921
+ throw new Error("quota cache exceeds bounds");
922
+ } finally { closeSync(fd); }
923
+ }
924
+
925
+ /** Cached pool observations only. An unavailable identity never authorizes publication or deletion. */
926
+ export function getAccountQuotaHistory(accountId: string, limit: number = QUOTA_HISTORY_LIMITS.perAccount) {
927
+ hydrateAccountQuotasFromDisk();
928
+ const result = quotaHistory.read(accountId, poolQuotaHistoryIdentity(accountId), Date.now(), limit);
929
+ return { observations: result.samples.map(({ credentialGeneration: _generation, ...sample }) => sample),
930
+ truncated: result.truncated, retention: { maxObservations: QUOTA_HISTORY_LIMITS.perAccount, maxAgeDays: 30 } };
931
+ }
@@ -13,6 +13,7 @@ export interface CodexCatalogRefreshResult {
13
13
  catalogWritten: boolean;
14
14
  cacheSynced: boolean;
15
15
  comboOmissions: ComboCatalogOmission[];
16
+ refreshOutcome?: "committed" | "refused";
16
17
  /** Desired OFF observed under K during the catalog commit; no cache write either. */
17
18
  skippedReason?: "desired_disabled";
18
19
  }
@@ -49,7 +50,7 @@ export async function refreshCodexModelCatalog(
49
50
  const catalogExists = deps.existsSync(result.path);
50
51
  const catalogWritten = result.catalogWritten === true;
51
52
  const comboOmissions = result.comboOmissions ?? [];
52
- if (result.skippedReason === "desired_disabled") {
53
+ if (result.skippedReason === "desired_disabled" || result.refreshOutcome === "refused") {
53
54
  // The commit path observed OFF under K. Invalidate nothing: rewriting the
54
55
  // models cache here would be exactly the routed-cache write the skip refused.
55
56
  return { ...result, catalogExists, catalogWritten: false, cacheSynced: false, comboOmissions };