@yansigit/opencodex 2.36.1-dev.20260829.47 → 2.36.1-dev.20260829.50

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 (112) hide show
  1. package/gui/dist/assets/{ApiKeys-DAuYQsGJ.js → ApiKeys-BLPOvK4z.js} +1 -1
  2. package/gui/dist/assets/{Claude-bN_80RT8.js → Claude-BbHv3EU7.js} +1 -1
  3. package/gui/dist/assets/{CodexSet-D3dWX_ay.js → CodexSet-hgu5hES-.js} +1 -1
  4. package/gui/dist/assets/{FileIntegrationPage-BGyzkPs2.js → FileIntegrationPage-RCmDvvsx.js} +1 -1
  5. package/gui/dist/assets/{Grok-CuaKd1TT.js → Grok-DH-ggRqA.js} +1 -1
  6. package/gui/dist/assets/{Integrations-C7NGQoxR.js → Integrations-D6m5E2-T.js} +2 -2
  7. package/gui/dist/assets/{IntegrationsOverview-DjVorYO5.js → IntegrationsOverview-C1rbnXV7.js} +1 -1
  8. package/gui/dist/assets/{Logs-B21a-9xe.js → Logs-Bqb0vYRk.js} +1 -1
  9. package/gui/dist/assets/{Models-CN7Q3kuK.js → Models-gMnzHf0c.js} +1 -1
  10. package/gui/dist/assets/{NumberStepper-BWg90tEs.js → NumberStepper-awaoZNlj.js} +1 -1
  11. package/gui/dist/assets/{Providers-CP_i-7LS.js → Providers-Bo6wuQMa.js} +1 -1
  12. package/gui/dist/assets/{RestoreDialog-3M0NqCiD.js → RestoreDialog-DX7SGWIk.js} +1 -1
  13. package/gui/dist/assets/{Startup-BVPFoP2s.js → Startup-B1htiLU9.js} +1 -1
  14. package/gui/dist/assets/{Storage-BdsG9ypz.js → Storage-B3MfosmS.js} +1 -1
  15. package/gui/dist/assets/{Subagents-CguR_bGQ.js → Subagents-L6Q2V9PZ.js} +1 -1
  16. package/gui/dist/assets/{Usage-C0kKi9_H.js → Usage-xbZnkDoj.js} +1 -1
  17. package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
  18. package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
  19. package/gui/dist/assets/{data-surface-BCycbObp.js → data-surface-BbBMp0Wj.js} +1 -1
  20. package/gui/dist/assets/{index-CU1jE0st.js → index-JEC_6CUW.js} +3 -3
  21. package/gui/dist/assets/{model-display-D2elXkuE.js → model-display-BvLWy9D5.js} +1 -1
  22. package/gui/dist/assets/{provider-payload-DfkXWE7v.js → provider-payload-BRfSsonS.js} +1 -1
  23. package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
  24. package/gui/dist/assets/shared-DI18-uF7.js +69 -0
  25. package/gui/dist/index.html +2 -2
  26. package/package.json +3 -1
  27. package/src/adapters/base.ts +26 -0
  28. package/src/adapters/cursor/catalog.ts +541 -0
  29. package/src/adapters/cursor/cursor-errors.ts +15 -0
  30. package/src/adapters/cursor/discovery.ts +34 -41
  31. package/src/adapters/cursor/envelope-echo.ts +128 -0
  32. package/src/adapters/cursor/request-builder.ts +19 -12
  33. package/src/adapters/cursor/tool-definitions.ts +2 -1
  34. package/src/adapters/cursor/tool-result-normalize.ts +23 -31
  35. package/src/adapters/cursor.ts +21 -2
  36. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  37. package/src/adapters/google-antigravity-replay.ts +71 -2
  38. package/src/adapters/google.ts +19 -4
  39. package/src/adapters/kiro-constants.ts +12 -0
  40. package/src/adapters/kiro.ts +128 -11
  41. package/src/adapters/openai-chat.ts +16 -2
  42. package/src/adapters/openai-responses.ts +15 -2
  43. package/src/adapters/tool-catalog-nudge.ts +2 -1
  44. package/src/adapters/xai-web-search.ts +10 -14
  45. package/src/claude/outbound.ts +14 -3
  46. package/src/cli/access.ts +46 -3
  47. package/src/cli/account-api.ts +93 -16
  48. package/src/cli/account-extended.ts +262 -36
  49. package/src/cli/account-main.ts +12 -12
  50. package/src/cli/account.ts +40 -10
  51. package/src/cli/agent.ts +8 -1
  52. package/src/cli/capabilities-command.ts +94 -0
  53. package/src/cli/capabilities.ts +535 -0
  54. package/src/cli/claude-desktop.ts +31 -11
  55. package/src/cli/config-command.ts +7 -3
  56. package/src/cli/dispatch.ts +195 -27
  57. package/src/cli/doctor.ts +100 -1
  58. package/src/cli/help.ts +11 -2
  59. package/src/cli/index.ts +19 -3
  60. package/src/cli/init.ts +2 -2
  61. package/src/cli/inspect.ts +230 -0
  62. package/src/cli/observe.ts +11 -3
  63. package/src/cli/provider.ts +58 -74
  64. package/src/cli/registry.ts +34 -2
  65. package/src/cli/runtime-api.ts +51 -7
  66. package/src/cli/status.ts +16 -0
  67. package/src/cli/storage.ts +234 -0
  68. package/src/cli/system-command.ts +16 -0
  69. package/src/cli/usage-report.ts +52 -2
  70. package/src/cli/version-skew.ts +46 -0
  71. package/src/codex/account-label.ts +21 -0
  72. package/src/codex/catalog/provider-fetch.ts +4 -0
  73. package/src/codex/convergence.ts +110 -15
  74. package/src/codex/transition-state.ts +12 -3
  75. package/src/compatibility/openai-responses.ts +9 -1
  76. package/src/config.ts +209 -11
  77. package/src/generated/compatibility-version.json +115 -75
  78. package/src/integrations/ownership-policy.ts +24 -5
  79. package/src/integrations/ownership.ts +36 -2
  80. package/src/integrations/state.ts +40 -7
  81. package/src/integrations/writer.ts +21 -3
  82. package/src/lib/admin-secrets.ts +24 -0
  83. package/src/lib/errors.ts +25 -1
  84. package/src/lib/service-secrets.ts +15 -0
  85. package/src/oauth/index.ts +143 -82
  86. package/src/oauth/login-cli.ts +53 -11
  87. package/src/oauth/store.ts +16 -6
  88. package/src/providers/alibaba-region-startup.ts +31 -11
  89. package/src/providers/api-keys.ts +58 -34
  90. package/src/providers/key-failover.ts +59 -46
  91. package/src/providers/label.ts +34 -1
  92. package/src/providers/model-rename-startup.ts +25 -7
  93. package/src/providers/openai-tier-startup.ts +38 -14
  94. package/src/responses/turn-termination.ts +107 -0
  95. package/src/server/index.ts +10 -0
  96. package/src/server/management/context.ts +10 -3
  97. package/src/server/management/logs-usage-routes.ts +24 -43
  98. package/src/server/management/model-routes.ts +284 -165
  99. package/src/server/management/oauth-account-routes.ts +93 -15
  100. package/src/server/management/provider-routes.ts +200 -87
  101. package/src/server/management/route-registry.ts +317 -0
  102. package/src/server/proxy-liveness.ts +27 -4
  103. package/src/server/request-log.ts +37 -17
  104. package/src/server/responses/core.ts +80 -4
  105. package/src/server/responses/policy-fallback.ts +1 -1
  106. package/src/service.ts +34 -0
  107. package/src/storage/policy-input.ts +166 -0
  108. package/src/storage/policy-job.ts +14 -4
  109. package/src/storage/policy.ts +115 -221
  110. package/src/usage/log.ts +32 -6
  111. package/src/usage/summary.ts +22 -34
  112. package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
@@ -8,7 +8,7 @@
8
8
  * Privacy: logs never include host paths, digests of file contents, or secrets.
9
9
  */
10
10
  import { resolveCodexHomeDir } from "../codex/home";
11
- import { loadConfig, saveConfigPreservingClaudeCode } from "../config";
11
+ import { loadConfig, mutatePersistedConfig } from "../config";
12
12
  import type { StorageCleanupPolicy } from "../types";
13
13
  import {
14
14
  computePreviewDigest,
@@ -22,9 +22,21 @@ import {
22
22
  type CleanupResult,
23
23
  type ExecuteCleanupOptions,
24
24
  } from "./cleanup";
25
+ import {
26
+ computeNextRun,
27
+ normalizeStorageCleanupPolicy,
28
+ } from "./policy-input";
29
+ export {
30
+ computeNextRun,
31
+ defaultStorageCleanupPolicy,
32
+ isValidPolicyTarget,
33
+ normalizeStorageCleanupPolicy,
34
+ parseStorageCleanupPolicyInput,
35
+ DEFAULT_ARCHIVED_BYTES_OVER,
36
+ DEFAULT_REMOVE_OLDEST_PERCENT,
37
+ type PolicySchedule,
38
+ } from "./policy-input";
25
39
 
26
- export const DEFAULT_ARCHIVED_BYTES_OVER = 5 * 1024 ** 3; // 5 GiB
27
- export const DEFAULT_REMOVE_OLDEST_PERCENT = 25;
28
40
  export const BUSY_DEFER_MS = 15 * 60 * 1000;
29
41
 
30
42
  /** Optional sink so background policy writes stay synced with the live server config. */
@@ -36,8 +48,12 @@ export function setStorageCleanupPolicyLiveSink(
36
48
  livePolicySink = sink ?? undefined;
37
49
  }
38
50
 
39
- export type PolicySchedule = StorageCleanupPolicy["schedule"];
51
+ function adoptStorageCleanupPolicy(policy: StorageCleanupPolicy): void {
52
+ livePolicySink?.(policy);
53
+ }
54
+
40
55
  export type PolicyRunReason = "startup" | "schedule" | "manual";
56
+ export type PolicyMetadataPersistenceError = "missing" | "invalid" | "conflict" | "write_failed";
41
57
 
42
58
  export type PolicySkipReason =
43
59
  | "disabled"
@@ -54,6 +70,7 @@ export interface PolicyRunResult {
54
70
  freedBytes?: number;
55
71
  removed?: number;
56
72
  trashDir?: string;
73
+ metadataPersistenceError?: PolicyMetadataPersistenceError;
57
74
  policy: StorageCleanupPolicy;
58
75
  }
59
76
 
@@ -76,206 +93,20 @@ export interface PolicyRunDeps {
76
93
  onPolicyLoaded?: () => void;
77
94
  }
78
95
 
79
- /** Canonical defaults — enabled is always false. */
80
- export function defaultStorageCleanupPolicy(): StorageCleanupPolicy {
81
- return {
82
- enabled: false,
83
- trigger: { archivedBytesOver: DEFAULT_ARCHIVED_BYTES_OVER },
84
- target: { removeOldestPercent: DEFAULT_REMOVE_OLDEST_PERCENT },
85
- schedule: "manual",
86
- mode: "quarantine",
87
- };
88
- }
89
-
90
- function isFiniteNonNegInt(n: unknown): n is number {
91
- return typeof n === "number" && Number.isFinite(n) && n >= 0 && Math.floor(n) === n;
92
- }
93
-
94
- function isFinitePositiveInt(n: unknown): n is number {
95
- return typeof n === "number" && Number.isFinite(n) && n > 0 && Math.floor(n) === n;
96
- }
97
-
98
- /** True when target has exactly one of reduceToBytes / removeOldestPercent. */
99
- export function isValidPolicyTarget(target: StorageCleanupPolicy["target"]): boolean {
100
- if (!target || typeof target !== "object") return false;
101
- const reduce = (target as { reduceToBytes?: unknown }).reduceToBytes;
102
- const percent = (target as { removeOldestPercent?: unknown }).removeOldestPercent;
103
- const hasReduce = reduce !== undefined;
104
- const hasPercent = percent !== undefined;
105
- if (hasReduce === hasPercent) return false; // none or both
106
- if (hasReduce) return isFiniteNonNegInt(reduce);
107
- return typeof percent === "number" && Number.isFinite(percent) && percent > 0 && percent <= 100;
108
- }
109
-
110
- /**
111
- * Normalize a partial/unknown policy into a complete StorageCleanupPolicy.
112
- * Never flips enabled to true unless the input explicitly sets enabled: true.
113
- */
114
- export function normalizeStorageCleanupPolicy(raw: unknown): StorageCleanupPolicy {
115
- const base = defaultStorageCleanupPolicy();
116
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return base;
117
- const o = raw as Record<string, unknown>;
118
-
119
- // only explicit true enables — and never when a present target is malformed
120
- let enabled = o.enabled === true;
121
-
122
- let archivedBytesOver = base.trigger.archivedBytesOver;
123
- if (o.trigger && typeof o.trigger === "object" && !Array.isArray(o.trigger)) {
124
- const t = (o.trigger as { archivedBytesOver?: unknown }).archivedBytesOver;
125
- if (isFiniteNonNegInt(t)) archivedBytesOver = t;
126
- }
127
-
128
- let target: StorageCleanupPolicy["target"] = base.target;
129
- if (Object.prototype.hasOwnProperty.call(o, "target")) {
130
- if (o.target && typeof o.target === "object" && !Array.isArray(o.target)) {
131
- const candidate = o.target as StorageCleanupPolicy["target"];
132
- if (isValidPolicyTarget(candidate)) {
133
- const reduce = (candidate as { reduceToBytes?: number }).reduceToBytes;
134
- const percent = (candidate as { removeOldestPercent?: number }).removeOldestPercent;
135
- target = reduce !== undefined
136
- ? { reduceToBytes: reduce }
137
- : { removeOldestPercent: Math.min(100, Math.max(1, Math.floor(percent!))) };
138
- } else {
139
- // Fail closed: malformed persisted target must not become delete-oldest 25%.
140
- enabled = false;
141
- }
142
- } else {
143
- enabled = false;
144
- }
145
- }
146
-
147
- const schedule: PolicySchedule =
148
- o.schedule === "startup" || o.schedule === "daily" || o.schedule === "weekly" || o.schedule === "manual"
149
- ? o.schedule
150
- : base.schedule;
151
-
152
- const mode: CleanupMode = o.mode === "permanent" ? "permanent" : "quarantine";
153
-
154
- let lastRun: StorageCleanupPolicy["lastRun"];
155
- if (o.lastRun && typeof o.lastRun === "object" && !Array.isArray(o.lastRun)) {
156
- const lr = o.lastRun as Record<string, unknown>;
157
- if (
158
- isFinitePositiveInt(lr.at)
159
- && isFiniteNonNegInt(lr.freedBytes)
160
- && isFiniteNonNegInt(lr.removed)
161
- ) {
162
- lastRun = { at: lr.at, freedBytes: lr.freedBytes, removed: lr.removed };
163
- }
164
- }
165
-
166
- let nextRun: number | undefined;
167
- if (o.nextRun === undefined || o.nextRun === null) {
168
- nextRun = undefined;
169
- } else if (isFinitePositiveInt(o.nextRun)) {
170
- nextRun = o.nextRun;
171
- }
172
-
173
- return {
174
- enabled,
175
- trigger: { archivedBytesOver },
176
- target,
177
- schedule,
178
- mode,
179
- ...(lastRun ? { lastRun } : {}),
180
- ...(nextRun !== undefined ? { nextRun } : {}),
181
- };
182
- }
183
-
184
- /**
185
- * Validate a PUT body. Returns a normalized policy or an error string.
186
- * Does not invent enabled=true.
187
- */
188
- export function parseStorageCleanupPolicyInput(
189
- raw: unknown,
190
- previous?: StorageCleanupPolicy,
191
- ): { ok: true; policy: StorageCleanupPolicy } | { ok: false; error: string } {
192
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
193
- return { ok: false, error: "body must be a JSON object" };
194
- }
195
- const o = raw as Record<string, unknown>;
196
- const prev = previous ?? defaultStorageCleanupPolicy();
197
-
198
- if (o.enabled !== undefined && typeof o.enabled !== "boolean") {
199
- return { ok: false, error: "enabled must be a boolean" };
200
- }
201
- if (o.mode !== undefined && o.mode !== "quarantine" && o.mode !== "permanent") {
202
- return { ok: false, error: "mode must be quarantine or permanent" };
203
- }
204
- if (
205
- o.schedule !== undefined
206
- && o.schedule !== "startup"
207
- && o.schedule !== "daily"
208
- && o.schedule !== "weekly"
209
- && o.schedule !== "manual"
210
- ) {
211
- return { ok: false, error: "schedule must be startup, daily, weekly, or manual" };
212
- }
213
-
214
- const merged: Record<string, unknown> = {
215
- ...prev,
216
- ...o,
217
- trigger: o.trigger !== undefined ? o.trigger : prev.trigger,
218
- target: o.target !== undefined ? o.target : prev.target,
219
- // Preserve run metadata unless the client explicitly sends replacements.
220
- lastRun: o.lastRun !== undefined ? o.lastRun : prev.lastRun,
221
- nextRun: o.nextRun !== undefined ? o.nextRun : prev.nextRun,
222
- };
223
-
224
- if (o.trigger !== undefined) {
225
- if (!o.trigger || typeof o.trigger !== "object" || Array.isArray(o.trigger)) {
226
- return { ok: false, error: "trigger must be an object" };
227
- }
228
- const bytes = (o.trigger as { archivedBytesOver?: unknown }).archivedBytesOver;
229
- if (!isFiniteNonNegInt(bytes)) {
230
- return { ok: false, error: "trigger.archivedBytesOver must be a non-negative integer" };
231
- }
232
- }
233
-
234
- if (o.target !== undefined) {
235
- if (!isValidPolicyTarget(o.target as StorageCleanupPolicy["target"])) {
236
- return {
237
- ok: false,
238
- error: "target must set exactly one of reduceToBytes (non-negative int) or removeOldestPercent (1-100)",
239
- };
240
- }
241
- }
242
-
243
- // Clients must not clear lastRun/nextRun via null unless intentional — accept omit only.
244
- const policy = normalizeStorageCleanupPolicy(merged);
245
- // Recompute nextRun only when schedule newly becomes timed, or no valid nextRun exists.
246
- // Do not reset nextRun on every PUT that merely re-sends an unchanged schedule.
247
- if (
248
- (policy.schedule === "daily" || policy.schedule === "weekly")
249
- && o.nextRun === undefined
250
- && (policy.nextRun === undefined || (o.schedule !== undefined && o.schedule !== prev.schedule))
251
- ) {
252
- policy.nextRun = computeNextRun(policy.schedule, Date.now());
253
- }
254
- if (policy.schedule === "manual" || policy.schedule === "startup") {
255
- if (o.nextRun === undefined) delete policy.nextRun;
256
- }
257
-
258
- return { ok: true, policy };
259
- }
260
-
261
96
  export function readStorageCleanupPolicyFromConfig(): StorageCleanupPolicy {
262
97
  return normalizeStorageCleanupPolicy(loadConfig().storageCleanupPolicy);
263
98
  }
264
99
 
265
100
  export function writeStorageCleanupPolicyToConfig(policy: StorageCleanupPolicy): StorageCleanupPolicy {
266
101
  const normalized = normalizeStorageCleanupPolicy(policy);
267
- const config = loadConfig();
268
- config.storageCleanupPolicy = normalized;
269
- saveConfigPreservingClaudeCode(config);
270
- livePolicySink?.(normalized);
271
- return normalized;
272
- }
273
-
274
- /** Wall-clock next run for daily/weekly. Startup/manual → undefined. */
275
- export function computeNextRun(schedule: PolicySchedule, now: number): number | undefined {
276
- if (schedule === "daily") return now + 24 * 60 * 60 * 1000;
277
- if (schedule === "weekly") return now + 7 * 24 * 60 * 60 * 1000;
278
- return undefined;
102
+ const outcome = mutatePersistedConfig(fresh => {
103
+ const changed = JSON.stringify(fresh.storageCleanupPolicy) !== JSON.stringify(normalized);
104
+ fresh.storageCleanupPolicy = normalized;
105
+ return { changed, value: structuredClone(normalized) };
106
+ });
107
+ if (outcome.status === "unavailable") throw new Error(`storage cleanup policy persistence unavailable: ${outcome.reason}`);
108
+ adoptStorageCleanupPolicy(outcome.value);
109
+ return outcome.value;
279
110
  }
280
111
 
281
112
  export function isPolicyDue(
@@ -401,6 +232,21 @@ export type PolicyRunMetadataPatch = {
401
232
  lastRun?: StorageCleanupPolicy["lastRun"];
402
233
  };
403
234
 
235
+ /** Apply only run-owned fields while preserving the supplied policy settings. */
236
+ function applyPolicyRunMetadata(
237
+ policy: StorageCleanupPolicy,
238
+ patch: PolicyRunMetadataPatch,
239
+ ): StorageCleanupPolicy {
240
+ let next =
241
+ patch.nextRun === "defer_busy"
242
+ ? deferBusy(policy, patch.now)
243
+ : advanceNextRun(policy, patch.now);
244
+ if (patch.lastRun) {
245
+ next = { ...next, lastRun: patch.lastRun };
246
+ }
247
+ return next;
248
+ }
249
+
404
250
  /**
405
251
  * Reload the latest persisted policy and write only run-owned metadata
406
252
  * (`lastRun` / `nextRun`). Preserves concurrent edits to enabled, trigger,
@@ -412,17 +258,58 @@ export function commitPolicyRunMetadata(
412
258
  patch: PolicyRunMetadataPatch,
413
259
  ): StorageCleanupPolicy {
414
260
  const latest = normalizeStorageCleanupPolicy(load());
415
- let next =
416
- patch.nextRun === "defer_busy"
417
- ? deferBusy(latest, patch.now)
418
- : advanceNextRun(latest, patch.now);
419
- if (patch.lastRun) {
420
- next = { ...next, lastRun: patch.lastRun };
421
- }
261
+ const next = applyPolicyRunMetadata(latest, patch);
422
262
  save(next);
423
263
  return next;
424
264
  }
425
265
 
266
+ type PolicyRunMetadataCommit = {
267
+ policy: StorageCleanupPolicy;
268
+ persistenceError?: PolicyMetadataPersistenceError;
269
+ };
270
+
271
+ /** Attach the durable metadata outcome without replacing cleanup status or metrics. */
272
+ function withMetadataCommit(
273
+ result: Omit<PolicyRunResult, "policy" | "metadataPersistenceError">,
274
+ committed: PolicyRunMetadataCommit,
275
+ ): PolicyRunResult {
276
+ return {
277
+ ...result,
278
+ policy: committed.policy,
279
+ ...(committed.persistenceError ? { metadataPersistenceError: committed.persistenceError } : {}),
280
+ };
281
+ }
282
+
283
+ /** Recompute run-owned metadata from the latest config inside the mutation lock. */
284
+ function commitPolicyRunMetadataToConfig(
285
+ patch: PolicyRunMetadataPatch,
286
+ fallbackPolicy: StorageCleanupPolicy,
287
+ ): PolicyRunMetadataCommit {
288
+ const unavailable = (reason: PolicyMetadataPersistenceError): PolicyRunMetadataCommit => {
289
+ console.warn(`[storage-policy] metadata_persist_failed reason=${reason}`);
290
+ return {
291
+ policy: applyPolicyRunMetadata(fallbackPolicy, patch),
292
+ persistenceError: reason,
293
+ };
294
+ };
295
+ try {
296
+ const outcome = mutatePersistedConfig(config => {
297
+ const before = JSON.stringify(config.storageCleanupPolicy);
298
+ const next = applyPolicyRunMetadata(
299
+ normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
300
+ patch,
301
+ );
302
+ config.storageCleanupPolicy = next;
303
+ return { changed: JSON.stringify(next) !== before, value: structuredClone(next) };
304
+ });
305
+ if (outcome.status === "unavailable") return unavailable(outcome.reason);
306
+ adoptStorageCleanupPolicy(outcome.value);
307
+ return { policy: outcome.value };
308
+ } catch {
309
+ return unavailable("write_failed");
310
+ }
311
+ }
312
+
426
313
  function logPolicyEvent(message: string): void {
427
314
  console.log(`[storage-policy] ${message}`);
428
315
  }
@@ -434,10 +321,15 @@ function logPolicyEvent(message: string): void {
434
321
  export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
435
322
  const now = deps.now ?? Date.now();
436
323
  const load = deps.loadPolicy ?? readStorageCleanupPolicyFromConfig;
437
- const save = deps.savePolicy ?? writeStorageCleanupPolicyToConfig;
438
324
  const execute = deps.execute ?? executeArchivedCleanup;
439
-
440
325
  const policy = normalizeStorageCleanupPolicy(load());
326
+ // Injected stores retain the existing load/save contract. The production path
327
+ // recomputes metadata from the latest persisted policy inside the config lock.
328
+ const commitMetadata = deps.savePolicy !== undefined
329
+ ? (patch: PolicyRunMetadataPatch): PolicyRunMetadataCommit => ({
330
+ policy: commitPolicyRunMetadata(load, deps.savePolicy!, patch),
331
+ })
332
+ : (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadataToConfig(patch, policy);
441
333
  deps.onPolicyLoaded?.();
442
334
 
443
335
  if (typeof deps.holdAfterLoadMs === "number" && Number.isFinite(deps.holdAfterLoadMs) && deps.holdAfterLoadMs > 0) {
@@ -454,15 +346,15 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
454
346
 
455
347
  const selection = selectPolicyPreview(policy, deps.codexHome);
456
348
  if (selection.archivedBytes <= policy.trigger.archivedBytesOver) {
457
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
349
+ const committed = commitMetadata({ now, nextRun: "advance" });
458
350
  logPolicyEvent("skip under_threshold");
459
- return { ok: true, skipped: "under_threshold", policy: saved };
351
+ return withMetadataCommit({ ok: true, skipped: "under_threshold" }, committed);
460
352
  }
461
353
 
462
354
  if (selection.count === 0) {
463
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
355
+ const committed = commitMetadata({ now, nextRun: "advance" });
464
356
  logPolicyEvent("skip nothing_selected");
465
- return { ok: true, skipped: "nothing_selected", policy: saved };
357
+ return withMetadataCommit({ ok: true, skipped: "nothing_selected" }, committed);
466
358
  }
467
359
 
468
360
  const result = execute({
@@ -476,25 +368,28 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
476
368
  });
477
369
 
478
370
  if (!result.ok && result.error === "codex_busy") {
479
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "defer_busy" });
371
+ const committed = commitMetadata({ now, nextRun: "defer_busy" });
480
372
  logPolicyEvent("defer codex_busy");
481
- return { ok: false, deferred: "codex_busy", error: "codex_busy", policy: saved };
373
+ return withMetadataCommit({
374
+ ok: false,
375
+ deferred: "codex_busy",
376
+ error: "codex_busy",
377
+ }, committed);
482
378
  }
483
379
 
484
380
  if (!result.ok) {
485
381
  // Non-busy failure: still advance schedule so we do not tight-loop.
486
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
382
+ const committed = commitMetadata({ now, nextRun: "advance" });
487
383
  logPolicyEvent(`fail ${result.error ?? "cleanup_failed"}`);
488
- return {
384
+ return withMetadataCommit({
489
385
  ok: false,
490
386
  error: result.error,
491
387
  mode: result.mode,
492
388
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
493
- policy: saved,
494
- };
389
+ }, committed);
495
390
  }
496
391
 
497
- const saved = commitPolicyRunMetadata(load, save, {
392
+ const committed = commitMetadata({
498
393
  now,
499
394
  nextRun: "advance",
500
395
  lastRun: {
@@ -506,14 +401,13 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
506
401
  logPolicyEvent(
507
402
  `ok mode=${result.mode} removed=${result.count} freedBytes=${result.bytes}`,
508
403
  );
509
- return {
404
+ return withMetadataCommit({
510
405
  ok: true,
511
406
  mode: result.mode,
512
407
  freedBytes: result.bytes,
513
408
  removed: result.count,
514
409
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
515
- policy: saved,
516
- };
410
+ }, committed);
517
411
  }
518
412
 
519
413
  /** Startup / schedule tick entry — swallows unexpected errors. */
package/src/usage/log.ts CHANGED
@@ -8,22 +8,27 @@ import { sanitizeLogMetadataString } from "../lib/redact";
8
8
  import { usageDisplayTotalTokens } from "./totals";
9
9
  import type { AttemptTierOutcome, OcxUsage } from "../types";
10
10
  import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
11
- import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
11
+ import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
12
12
  import type { AgentKind } from "../server/effort-policy";
13
13
 
14
14
  export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
15
- export type CodexUsageAccountLogLabel = "main" | `p${string}`;
15
+ export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}`;
16
+ export type CodexUsageAccountLogLabel = UsageAccountLogLabel;
16
17
 
17
18
  export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^[a-f0-9]{8,64}(?:-\d+)?$/;
18
19
 
19
20
  export function isPersistableAccountLogLabel(value: unknown): value is string {
20
21
  return (
21
- value === "main"
22
- || (typeof value === "string" && (CODEX_ACCOUNT_LOG_LABEL_RE.test(value) || OAUTH_ACCOUNT_LOG_LABEL_RE.test(value)))
22
+ typeof value === "string"
23
+ && (ACCOUNT_LOG_LABEL_RE.test(value) || OAUTH_ACCOUNT_LOG_LABEL_RE.test(value))
23
24
  );
24
25
  }
25
26
 
26
- export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel {
27
+ export function isCodexUsageAccountLogLabel(value: unknown): value is UsageAccountLogLabel {
28
+ return typeof value === "string" && ACCOUNT_LOG_LABEL_RE.test(value);
29
+ }
30
+
31
+ export function isCodexPoolAccountLogLabel(value: unknown): value is "main" | `p${string}` {
27
32
  return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
28
33
  }
29
34
 
@@ -63,6 +68,13 @@ export interface PersistedUsageAttempt {
63
68
  sendCount: number;
64
69
  recoveryKinds: AttemptRecoveryKind[];
65
70
  usageStatus: UsageStatus;
71
+ /**
72
+ * True when the proxy answered this turn locally and issued no upstream request. It travels on
73
+ * the attempt itself rather than as a `finishRequestAttempt` argument because that function is
74
+ * called from six places, and a new parameter would silently default to the wrong answer at any
75
+ * one of them that was missed. Absent on ordinary attempts so old rows keep their exact shape.
76
+ */
77
+ locallyAnswered?: boolean;
66
78
  /** Stable non-PII identity for the account that served this attempt. */
67
79
  accountLogLabel?: string;
68
80
  inputTokenEstimate?: number;
@@ -200,8 +212,20 @@ function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
200
212
  || providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
201
213
  }
202
214
 
203
- export function usageForFinalLog(provider: string, usage: OcxUsage | undefined): OcxUsage | undefined {
215
+ export function usageForFinalLog(
216
+ provider: string,
217
+ usage: OcxUsage | undefined,
218
+ /**
219
+ * True when the proxy answered this turn locally and issued no upstream request. Such a turn's
220
+ * zero counts are EXACT, so the provider-wide estimated marking must not apply: Kiro and Cursor
221
+ * are marked estimated because their adapters can only guess a real inference's usage, and a
222
+ * turn with no inference has nothing to guess. Without this, a no-send turn is indistinguishable
223
+ * from a real one whose usage frame never arrived.
224
+ */
225
+ locallyAnswered = false,
226
+ ): OcxUsage | undefined {
204
227
  if (!usage) return undefined;
228
+ if (locallyAnswered) return usage;
205
229
  if (usage.estimated || isEstimatedUsageProvider(provider)) return { ...usage, estimated: true };
206
230
  return usage;
207
231
  }
@@ -239,6 +263,8 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
239
263
  "key-429",
240
264
  "rate-limit-429",
241
265
  "anthropic-oauth-429",
266
+ "cursor-oauth-auth",
267
+ "cursor-oauth-429",
242
268
  "oauth-account-429",
243
269
  "image-413",
244
270
  "opaque-blob-rejection",
@@ -94,7 +94,6 @@ export interface UsageProvider {
94
94
 
95
95
  export interface UsageAccount {
96
96
  accountLogLabel: string;
97
- provider?: string;
98
97
  ambiguous: boolean;
99
98
  requests: number;
100
99
  attemptCount: number;
@@ -685,25 +684,22 @@ function legacyCodexAccountLabel(provider: string): string | null {
685
684
  return suffix ?? LEGACY_AMBIGUOUS_ACCOUNT_LABEL;
686
685
  }
687
686
 
688
- export function accountLabelForAttribution(
689
- provider: string,
690
- explicit: unknown,
691
- fallbackAccountByProvider?: Record<string, string>,
692
- ): string | null {
693
- if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
694
- const legacy = legacyCodexAccountLabel(provider);
695
- if (legacy) return legacy;
696
- const base = baseProviderLabel(provider);
697
- if (fallbackAccountByProvider && fallbackAccountByProvider[base]) {
698
- return fallbackAccountByProvider[base];
699
- }
700
- return null;
687
+ /**
688
+ * An explicitly stamped label of EITHER family is authoritative for any provider (#2699).
689
+ *
690
+ * No `o`-label branch is needed here: `isCodexUsageAccountLogLabel` now accepts both families,
691
+ * and adding a second predicate call would be a no-op guarded by a comment claiming otherwise.
692
+ *
693
+ * The legacy fallback stays openai-only on purpose. It infers an account from the PROVIDER
694
+ * string, and inferring for a non-Codex row would merge unrelated accounts under one label --
695
+ * so an unlabeled xai row is dropped from the account table rather than guessed at.
696
+ */
697
+ function accountLabelForAttribution(provider: string, explicit: unknown): string | null {
698
+ if (isCodexUsageAccountLogLabel(explicit)) return explicit;
699
+ return legacyCodexAccountLabel(provider);
701
700
  }
702
701
 
703
- function buildAccounts(
704
- entries: PersistedUsageEntry[],
705
- fallbackAccountByProvider?: Record<string, string>,
706
- ): UsageAccount[] {
702
+ function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
707
703
  const byLabel = new Map<string, UsageAccount>();
708
704
  const requestIds = new Map<string, Set<string>>();
709
705
 
@@ -716,13 +712,12 @@ function buildAccounts(
716
712
  totalTokens?: number;
717
713
  estimate: ReturnType<typeof estimateRequestCost>;
718
714
  }): void => {
719
- const label = accountLabelForAttribution(input.provider, input.accountLogLabel, fallbackAccountByProvider);
715
+ const label = accountLabelForAttribution(input.provider, input.accountLogLabel);
720
716
  if (!label) return;
721
717
  let row = byLabel.get(label);
722
718
  if (!row) {
723
719
  row = {
724
720
  accountLogLabel: label,
725
- provider: baseProviderLabel(input.provider),
726
721
  ambiguous: label === LEGACY_AMBIGUOUS_ACCOUNT_LABEL,
727
722
  requests: 0,
728
723
  attemptCount: 0,
@@ -823,7 +818,6 @@ export function summarizeUsage(
823
818
  range: UsageRange,
824
819
  now: number,
825
820
  surface: UsageSurface = "all",
826
- fallbackAccountByProvider?: Record<string, string>,
827
821
  ): UsageSummary {
828
822
  const { since } = rangeWindow(range, now);
829
823
  const filteredEntries = entries.filter(entry => {
@@ -853,7 +847,7 @@ export function summarizeUsage(
853
847
  days: buildDayGrid(range, since, now, filteredEntries),
854
848
  models: buildModels(filteredEntries, totals.totalTokens),
855
849
  providers: buildProviders(filteredEntries, totals.totalTokens),
856
- accounts: buildAccounts(filteredEntries, fallbackAccountByProvider),
850
+ accounts: buildAccounts(filteredEntries),
857
851
  };
858
852
  }
859
853
 
@@ -885,14 +879,12 @@ function normalizeFilterValue(input: string | null | undefined): string | null {
885
879
  */
886
880
  export function projectUsageSummary<T extends UsageSummary>(
887
881
  summary: T,
888
- filter: { provider?: string | null; model?: string | null; account?: string | null },
882
+ filter: { provider?: string | null; model?: string | null },
889
883
  entries?: PersistedUsageEntry[],
890
- fallbackAccountByProvider?: Record<string, string>,
891
884
  ): T & { filter?: UsageFilterEcho } {
892
885
  const provider = normalizeFilterValue(filter.provider);
893
886
  const model = normalizeFilterValue(filter.model);
894
- const account = normalizeFilterValue(filter.account);
895
- if (provider === null && model === null && account === null) return summary;
887
+ if (provider === null && model === null) return summary;
896
888
 
897
889
  // Re-summarise from the entries the summary was built from, rather than
898
890
  // projecting over its rows.
@@ -909,13 +901,9 @@ export function projectUsageSummary<T extends UsageSummary>(
909
901
  //
910
902
  // The entries are already in hand on every path that filters, so the honest
911
903
  // computation is also the simple one.
912
- const matches = (rowProvider: string, rowModel: string, rowAccount?: string | null): boolean => {
904
+ const matches = (rowProvider: string, rowModel: string): boolean => {
913
905
  if (provider !== null && baseProviderLabel(rowProvider).toLowerCase() !== provider) return false;
914
906
  if (model !== null && rowModel.toLowerCase() !== model) return false;
915
- if (account !== null && rowAccount !== undefined) {
916
- const effectiveAccount = accountLabelForAttribution(rowProvider, rowAccount, fallbackAccountByProvider);
917
- if (!effectiveAccount || effectiveAccount.toLowerCase() !== account) return false;
918
- }
919
907
  return true;
920
908
  };
921
909
 
@@ -931,10 +919,10 @@ export function projectUsageSummary<T extends UsageSummary>(
931
919
  const filtered: PersistedUsageEntry[] = [];
932
920
  for (const entry of source) {
933
921
  if (!entry.attempts?.length) {
934
- if (matches(entry.provider, antigravityUsageModel(entry.provider, entry.model), entry.accountLogLabel)) filtered.push(entry);
922
+ if (matches(entry.provider, antigravityUsageModel(entry.provider, entry.model))) filtered.push(entry);
935
923
  continue;
936
924
  }
937
- const attempts = entry.attempts.filter(a => matches(a.provider, antigravityUsageModel(a.provider, a.model), a.accountLogLabel ?? entry.accountLogLabel));
925
+ const attempts = entry.attempts.filter(a => matches(a.provider, antigravityUsageModel(a.provider, a.model)));
938
926
  if (attempts.length === 0) continue;
939
927
  // A combo is still counted once per participating model, so a filtered
940
928
  // request count can exceed the number of distinct requests. That is the
@@ -943,7 +931,7 @@ export function projectUsageSummary<T extends UsageSummary>(
943
931
  filtered.push({ ...entry, attempts });
944
932
  }
945
933
 
946
- const projected = summarizeUsage(filtered, summary.range, summary.generatedAt, summary.surface, fallbackAccountByProvider);
934
+ const projected = summarizeUsage(filtered, summary.range, summary.generatedAt, summary.surface);
947
935
  // matched reflects usage inside the requested WINDOW, not anywhere in the
948
936
  // log: summarizeUsage applies the range and surface predicates, and the CLI
949
937
  // uses this flag to decide between a table and "no usage recorded".