@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.1

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 (94) hide show
  1. package/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
  2. package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
  3. package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
  4. package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
  5. package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
  6. package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
  7. package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
  8. package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
  9. package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
  10. package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
  11. package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
  12. package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
  13. package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
  14. package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
  15. package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
  16. package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.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-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
  20. package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
  21. package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
  22. package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.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/dispatch.ts +195 -27
  56. package/src/cli/doctor.ts +100 -1
  57. package/src/cli/help.ts +11 -2
  58. package/src/cli/index.ts +19 -3
  59. package/src/cli/inspect.ts +230 -0
  60. package/src/cli/observe.ts +11 -3
  61. package/src/cli/registry.ts +34 -2
  62. package/src/cli/runtime-api.ts +51 -7
  63. package/src/cli/status.ts +16 -0
  64. package/src/cli/storage.ts +234 -0
  65. package/src/cli/system-command.ts +16 -0
  66. package/src/cli/usage-report.ts +52 -2
  67. package/src/cli/version-skew.ts +46 -0
  68. package/src/codex/account-label.ts +21 -0
  69. package/src/codex/catalog/provider-fetch.ts +4 -0
  70. package/src/codex/transition-state.ts +12 -3
  71. package/src/compatibility/openai-responses.ts +9 -1
  72. package/src/generated/compatibility-version.json +94 -58
  73. package/src/integrations/ownership-policy.ts +24 -5
  74. package/src/integrations/ownership.ts +36 -2
  75. package/src/integrations/state.ts +40 -7
  76. package/src/integrations/writer.ts +21 -3
  77. package/src/lib/admin-secrets.ts +24 -0
  78. package/src/lib/errors.ts +25 -1
  79. package/src/lib/service-secrets.ts +15 -0
  80. package/src/oauth/store.ts +16 -6
  81. package/src/providers/label.ts +34 -1
  82. package/src/responses/turn-termination.ts +107 -0
  83. package/src/server/management/logs-usage-routes.ts +5 -27
  84. package/src/server/management/route-registry.ts +317 -0
  85. package/src/server/proxy-liveness.ts +27 -4
  86. package/src/server/request-log.ts +37 -17
  87. package/src/server/responses/core.ts +80 -4
  88. package/src/server/responses/policy-fallback.ts +1 -1
  89. package/src/service.ts +34 -0
  90. package/src/storage/policy-job.ts +14 -4
  91. package/src/storage/policy.ts +79 -33
  92. package/src/usage/log.ts +32 -6
  93. package/src/usage/summary.ts +22 -34
  94. package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `ocx storage` — the archived-session cleanup, trash, and cleanup-policy surface (wp7).
3
+ *
4
+ * Every route here existed with no CLI caller, so reclaiming disk space was dashboard-only.
5
+ * Three of them delete or move operator data, and the rules for those are deliberate:
6
+ *
7
+ * 1. **Default to preview.** `ocx storage cleanup --percent N` runs the preview route and prints
8
+ * what WOULD be freed, then exits 0 having mutated nothing.
9
+ * 2. **`--yes` is required to mutate.** There is no interactive prompt: an agent cannot answer
10
+ * one, and a prompt an agent can answer is not a safety boundary.
11
+ * 3. **`--json` on the preview emits the candidate list**, so an agent can decide from data
12
+ * rather than from a sentence.
13
+ *
14
+ * This is the opposite of the GitHub star POST, which no flag can authorize: cleanup spends the
15
+ * operator's DATA, which they can delegate, while starring spends their IDENTITY, which they
16
+ * cannot delegate to an agent.
17
+ */
18
+ import {
19
+ CliUsageError,
20
+ printData,
21
+ rejectArgs,
22
+ runCliAction,
23
+ runtimeRequest,
24
+ summaryLines,
25
+ takeFlag,
26
+ takeIntegerOption,
27
+ takeOption,
28
+ type RuntimeApiDeps,
29
+ } from "./runtime-api";
30
+
31
+ const USAGE = `Usage:
32
+ ocx storage report [--json]
33
+ ocx storage cleanup --percent <0-100> [--mode <quarantine|permanent>] [--yes] [--json]
34
+ ocx storage trash [list] [--json]
35
+ ocx storage trash restore <entry-id> [--yes] [--json]
36
+ ocx storage policy [show] [--json]
37
+ ocx storage policy set [--enabled <true|false>] [--percent <0-100>]
38
+ [--mode <quarantine|permanent>] [--schedule <startup|daily|weekly|manual>] [--json]
39
+ ocx storage policy run [--yes] [--json]
40
+
41
+ Cleanup and restore MUTATE operator data and require --yes.
42
+ Without --yes, cleanup prints the preview and changes nothing.`;
43
+
44
+ /** The digest binds a run to the preview it was authorized against. */
45
+ interface CleanupPreview {
46
+ percent?: number;
47
+ count?: number;
48
+ bytes?: number;
49
+ digest?: string;
50
+ candidates?: { relPath?: string; bytes?: number }[];
51
+ }
52
+
53
+ function mib(bytes: number | undefined): string {
54
+ if (typeof bytes !== "number" || !Number.isFinite(bytes)) return "unknown size";
55
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
56
+ }
57
+
58
+ function previewLines(preview: CleanupPreview): string[] {
59
+ const lines = [
60
+ `Would remove ${preview.count ?? 0} archived session file(s), freeing ${mib(preview.bytes)}.`,
61
+ ];
62
+ for (const candidate of (preview.candidates ?? []).slice(0, 10)) {
63
+ lines.push(` ${candidate.relPath ?? "(unnamed)"} ${mib(candidate.bytes)}`);
64
+ }
65
+ const shown = Math.min((preview.candidates ?? []).length, 10);
66
+ if ((preview.count ?? 0) > shown) lines.push(` … and ${(preview.count ?? 0) - shown} more`);
67
+ lines.push("Nothing was deleted. Re-run with --yes to apply.");
68
+ return lines;
69
+ }
70
+
71
+ async function cleanup(argv: string[], deps: RuntimeApiDeps): Promise<void> {
72
+ const args = [...argv];
73
+ const wantsJson = takeFlag(args, "--json");
74
+ const confirmed = takeFlag(args, "--yes");
75
+ const percent = takeIntegerOption(args, "--percent", { min: 0 });
76
+ const mode = takeOption(args, "--mode") ?? "quarantine";
77
+ rejectArgs(args, USAGE);
78
+
79
+ if (percent === undefined) throw new CliUsageError("--percent is required", USAGE);
80
+ if (percent > 100) throw new CliUsageError("--percent must be between 0 and 100", USAGE);
81
+ if (mode !== "quarantine" && mode !== "permanent") {
82
+ throw new CliUsageError("--mode must be quarantine or permanent", USAGE);
83
+ }
84
+
85
+ // The preview runs in BOTH paths, and not only to be friendly: the mutating route requires the
86
+ // digest this call returns and rejects a stale one with 409 `stale_preview`. So the confirmed
87
+ // path cannot skip it, which conveniently means --yes and no --yes agree on what they mean.
88
+ const preview = await runtimeRequest<CleanupPreview>("/api/storage/cleanup/preview", {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json" },
91
+ body: JSON.stringify({ percent }),
92
+ }, deps);
93
+
94
+ if (!confirmed) {
95
+ printData(preview, wantsJson, previewLines(preview));
96
+ return;
97
+ }
98
+
99
+ if (!preview.digest) {
100
+ // Refuse rather than send an empty digest: the server would reject it, but a clear local
101
+ // message beats a 400 that looks like a bug in the verb.
102
+ throw new CliUsageError("the preview returned no digest, so the cleanup cannot be authorized", USAGE);
103
+ }
104
+
105
+ const result = await runtimeRequest("/api/storage/cleanup", {
106
+ method: "POST",
107
+ headers: { "content-type": "application/json" },
108
+ body: JSON.stringify({ percent, mode, digest: preview.digest }),
109
+ }, deps);
110
+ printData(result, wantsJson, summaryLines(result));
111
+ }
112
+
113
+ async function trash(argv: string[], deps: RuntimeApiDeps): Promise<void> {
114
+ const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
115
+ const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
116
+
117
+ if (action === "list") {
118
+ const args = [...rest];
119
+ const wantsJson = takeFlag(args, "--json");
120
+ rejectArgs(args, USAGE);
121
+ const result = await runtimeRequest("/api/storage/trash", {}, deps);
122
+ printData(result, wantsJson, summaryLines(result));
123
+ return;
124
+ }
125
+
126
+ if (action !== "restore") throw new CliUsageError(`unknown trash action ${action}`, USAGE);
127
+
128
+ const args = [...rest];
129
+ const wantsJson = takeFlag(args, "--json");
130
+ const confirmed = takeFlag(args, "--yes");
131
+ const id = args.shift();
132
+ rejectArgs(args, USAGE);
133
+ if (!id) throw new CliUsageError("a trash entry id is required", USAGE);
134
+
135
+ // Restore moves files back and reconciles database rows, and can collide with an existing
136
+ // destination, so it is gated like cleanup rather than treated as a read.
137
+ if (!confirmed) {
138
+ throw new CliUsageError(`restoring ${id} modifies stored sessions; pass --yes to confirm`, USAGE);
139
+ }
140
+
141
+ const result = await runtimeRequest("/api/storage/trash/restore", {
142
+ method: "POST",
143
+ headers: { "content-type": "application/json" },
144
+ body: JSON.stringify({ id }),
145
+ }, deps);
146
+ printData(result, wantsJson, summaryLines(result));
147
+ }
148
+
149
+ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
+ const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "show";
151
+ const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
152
+
153
+ if (action === "show") {
154
+ const args = [...rest];
155
+ const wantsJson = takeFlag(args, "--json");
156
+ rejectArgs(args, USAGE);
157
+ const result = await runtimeRequest("/api/storage/cleanup-policy", {}, deps);
158
+ printData(result, wantsJson, summaryLines(result));
159
+ return;
160
+ }
161
+
162
+ if (action === "set") {
163
+ const args = [...rest];
164
+ const wantsJson = takeFlag(args, "--json");
165
+ const enabled = takeOption(args, "--enabled");
166
+ const percent = takeIntegerOption(args, "--percent", { min: 0 });
167
+ const mode = takeOption(args, "--mode");
168
+ const schedule = takeOption(args, "--schedule");
169
+ rejectArgs(args, USAGE);
170
+
171
+ if (enabled !== undefined && enabled !== "true" && enabled !== "false") {
172
+ throw new CliUsageError("--enabled must be true or false", USAGE);
173
+ }
174
+ const body: Record<string, unknown> = {};
175
+ if (enabled !== undefined) body.enabled = enabled === "true";
176
+ if (percent !== undefined) body.percent = percent;
177
+ if (mode !== undefined) body.mode = mode;
178
+ if (schedule !== undefined) body.schedule = schedule;
179
+ if (Object.keys(body).length === 0) {
180
+ throw new CliUsageError("policy set needs at least one of --enabled, --percent, --mode, --schedule", USAGE);
181
+ }
182
+ // Values are NOT re-validated here beyond --enabled's shape. The server owns the mode and
183
+ // schedule vocabularies and returns a named 400; duplicating them is a second thing to
184
+ // keep in sync. `enabled` is checked because "--enabled maybe" would otherwise be sent as
185
+ // `false`, which is a wrong write rather than a rejected one.
186
+ const result = await runtimeRequest("/api/storage/cleanup-policy", {
187
+ method: "PUT",
188
+ headers: { "content-type": "application/json" },
189
+ body: JSON.stringify(body),
190
+ }, deps);
191
+ printData(result, wantsJson, summaryLines(result));
192
+ return;
193
+ }
194
+
195
+ if (action !== "run") throw new CliUsageError(`unknown policy action ${action}`, USAGE);
196
+
197
+ const args = [...rest];
198
+ const wantsJson = takeFlag(args, "--json");
199
+ const confirmed = takeFlag(args, "--yes");
200
+ rejectArgs(args, USAGE);
201
+ // `force: true` server-side: this run ignores the schedule and deletes now.
202
+ if (!confirmed) {
203
+ throw new CliUsageError("policy run deletes archived sessions now; pass --yes to confirm", USAGE);
204
+ }
205
+ const result = await runtimeRequest("/api/storage/cleanup-policy/run", { method: "POST" }, deps);
206
+ printData(result, wantsJson, summaryLines(result));
207
+ }
208
+
209
+ export async function handleStorageCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
210
+ const hasSub = argv[0] !== undefined && !argv[0].startsWith("-");
211
+ const sub = hasSub ? argv[0]! : "report";
212
+ const rest = hasSub ? argv.slice(1) : argv;
213
+ if (sub === "codex-logs") {
214
+ // Doctor and the Log Guard guides still document `ocx storage codex-logs …`.
215
+ // This module owns cleanup/trash/policy; log-guard stays on the observe handler.
216
+ const { handleObserveCommand } = await import("./observe");
217
+ return handleObserveCommand(["storage", "codex-logs", ...rest], deps);
218
+ }
219
+ return runCliAction(async () => {
220
+ if (sub === "report") {
221
+ const args = [...rest];
222
+ const wantsJson = takeFlag(args, "--json");
223
+ rejectArgs(args, USAGE);
224
+ const result = await runtimeRequest("/api/storage", {}, deps);
225
+ printData(result, wantsJson, summaryLines(result));
226
+ }
227
+ else if (sub === "cleanup") await cleanup(rest, deps);
228
+ else if (sub === "trash") await trash(rest, deps);
229
+ else if (sub === "policy") await policy(rest, deps);
230
+ else throw new CliUsageError(`unknown storage command ${sub}`, USAGE);
231
+ });
232
+ }
233
+
234
+ export const STORAGE_USAGE = USAGE;
@@ -17,6 +17,8 @@ const USAGE = `Usage:
17
17
  ocx system startup <health|install-service|install-shim> [--json]
18
18
  ocx system diagnostics [--json]
19
19
  ocx system sync [--json]
20
+ ocx system codex-app-server [--json]
21
+ ocx system codex-restart --yes [--json]
20
22
  ocx system update check [--channel <latest|preview>] [--json]
21
23
  ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
22
24
  ocx system update status <job-id> [--json]`;
@@ -104,6 +106,20 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
104
106
  } else if (sub === "sync") {
105
107
  const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
106
108
  printData(await runtimeRequest("/api/sync", { method: "POST" }, deps), wantsJson);
109
+ } else if (sub === "codex-app-server") {
110
+ // The GUI reads this state directly (gui/src/codex-app-server-state.ts). Without a verb
111
+ // an agent could not see whether the Codex app-server was reachable at all.
112
+ const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
113
+ printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
114
+ } else if (sub === "codex-restart") {
115
+ // --yes required: this restarts the user's running Codex app-server, so it is exactly the
116
+ // class of action that must not happen because an agent guessed a subcommand.
117
+ const args = [...rest];
118
+ const wantsJson = takeFlag(args, "--json");
119
+ const yes = takeFlag(args, "--yes");
120
+ if (!yes) throw new CliUsageError("system codex-restart requires --yes", USAGE);
121
+ rejectArgs(args, USAGE);
122
+ printData(await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps), wantsJson, ["Codex app-server restart requested."]);
107
123
  } else if (sub === "update") await update(rest, deps);
108
124
  else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
109
125
  });
@@ -38,10 +38,34 @@ interface UsageReportInput {
38
38
  providers?: CostRow[];
39
39
  days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
40
40
  filter?: { provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
41
+ /**
42
+ * Per-account totals the API already returns and the CLI discarded (#2700).
43
+ *
44
+ * Withheld by the server -- sent as `[]` -- whenever a provider or model filter is active,
45
+ * because account rows are not provider-partitioned in a way the projection could honestly
46
+ * re-derive (summary.ts, reasoned around :865-872). An empty array therefore means two very
47
+ * different things, and the renderer must say which.
48
+ */
49
+ accounts?: {
50
+ accountLogLabel: string;
51
+ ambiguous?: boolean;
52
+ requests: number;
53
+ totalTokens: number;
54
+ estimatedCostUsd?: number;
55
+ }[];
41
56
  }
42
57
 
43
58
  const MAX_MODEL_ROWS = 10;
44
59
 
60
+ function terminalText(value: string): string {
61
+ return value.replace(/[\x00-\x1f\x7f-\x9f]/g, character => {
62
+ const code = character.charCodeAt(0);
63
+ return code <= 0x7f
64
+ ? `\\x${code.toString(16).padStart(2, "0")}`
65
+ : `\\u${code.toString(16).padStart(4, "0")}`;
66
+ });
67
+ }
68
+
45
69
  function count(value: number | undefined): string {
46
70
  return (value ?? 0).toLocaleString("en-US");
47
71
  }
@@ -58,6 +82,8 @@ function usd(value: number | undefined): string {
58
82
 
59
83
  function table(header: string[], rows: string[][]): string[] {
60
84
  if (rows.length === 0) return [];
85
+ header = header.map(terminalText);
86
+ rows = rows.map(row => row.map(terminalText));
61
87
  const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
62
88
  const line = (cols: string[]): string => cols.map((c, i) => (c ?? "").padEnd(widths[i]!)).join(" ").trimEnd();
63
89
  return [line(header), ...rows.map(line)];
@@ -68,7 +94,7 @@ function describeScope(data: UsageReportInput): string {
68
94
  if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
69
95
  if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
70
96
  if (data.filter?.model) parts.push(`model=${data.filter.model}`);
71
- return parts.join(", ");
97
+ return terminalText(parts.join(", "));
72
98
  }
73
99
 
74
100
  export function formatUsageReport(data: UsageReportInput): string[] {
@@ -78,7 +104,7 @@ export function formatUsageReport(data: UsageReportInput): string[] {
78
104
  if (data.filter && !data.filter.matched) {
79
105
  const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
80
106
  .filter(Boolean).join(" and ");
81
- lines.push(`No usage recorded for ${what} in this range.`);
107
+ lines.push(`No usage recorded for ${terminalText(what)} in this range.`);
82
108
  lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
83
109
  return lines;
84
110
  }
@@ -110,6 +136,30 @@ export function formatUsageReport(data: UsageReportInput): string[] {
110
136
  ));
111
137
  }
112
138
 
139
+ // Per-account spend (#2700). The two empty cases are distinguished deliberately: printing an
140
+ // empty table under a filter would repeat the silently-wrong-output defect this unit removes,
141
+ // because "what did this provider cost me per account" is the most natural way to ask and the
142
+ // server cannot answer it honestly.
143
+ const accountFilterActive = Boolean(data.filter?.provider || data.filter?.model);
144
+ const accounts = (data.accounts ?? []).filter(row => row.requests > 0);
145
+ if (accountFilterActive) {
146
+ lines.push("");
147
+ lines.push("ACCOUNT: not reported under a provider or model filter; run without filters for per-account totals.");
148
+ } else if (accounts.length > 0) {
149
+ lines.push("");
150
+ lines.push(...table(
151
+ ["ACCOUNT", "REQUESTS", "TOKENS", "EST. COST"],
152
+ accounts.map(row => [
153
+ // An ambiguous row aggregates several accounts, so reading it as one account draws the
154
+ // wrong conclusion. Mark it rather than presenting it as a single identity.
155
+ row.ambiguous ? `${terminalText(row.accountLogLabel)} (ambiguous)` : terminalText(row.accountLogLabel),
156
+ count(row.requests),
157
+ count(row.totalTokens),
158
+ usd(row.estimatedCostUsd),
159
+ ]),
160
+ ));
161
+ }
162
+
113
163
  const models = (data.models ?? []).filter(row => row.requests > 0);
114
164
  if (models.length > 0) {
115
165
  lines.push("");
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CLI-versus-proxy version skew (#2701).
3
+ *
4
+ * The reported failure: `ocx` on PATH is an older install than the running proxy, so its
5
+ * help describes commands the proxy does not have and its output describes a different
6
+ * build. Nothing surfaced that, because the CLI never compared the two.
7
+ *
8
+ * Kept in its own module rather than inside `status.ts` so `doctor` can reuse the exact
9
+ * comparison instead of reimplementing it -- two diagnostics disagreeing about whether an
10
+ * install is stale would be worse than neither reporting it.
11
+ */
12
+
13
+ /** Placeholder versions that mean "unknown", not "different". */
14
+ const PLACEHOLDERS = new Set(["unknown", "0.0.0"]);
15
+
16
+ export interface VersionSkew {
17
+ readonly cliVersion: string;
18
+ /** Version the live proxy reported, or null when nothing is live or it reported none. */
19
+ readonly proxyVersion: string | null;
20
+ readonly skewed: boolean;
21
+ /** Operator-facing explanation; null when there is nothing to report. */
22
+ readonly warning: string | null;
23
+ }
24
+
25
+ /**
26
+ * Compare the running CLI against the live proxy.
27
+ *
28
+ * Suppressed rather than reported when either side is a placeholder. `packageVersion()`
29
+ * answers `"unknown"` when `package.json` carries no string version, and the server's
30
+ * `VERSION` falls back to `"0.0.0"` when it cannot resolve its own package -- comparing
31
+ * against either would report skew that says nothing about the install. A false stale-CLI
32
+ * warning would send an operator to reinstall a healthy setup.
33
+ */
34
+ export function computeVersionSkew(cliVersion: string, proxyVersion: string | undefined): VersionSkew {
35
+ const proxy = proxyVersion ?? null;
36
+ if (proxy === null || PLACEHOLDERS.has(proxy) || PLACEHOLDERS.has(cliVersion) || proxy === cliVersion) {
37
+ return { cliVersion, proxyVersion: proxy, skewed: false, warning: null };
38
+ }
39
+ return {
40
+ cliVersion,
41
+ proxyVersion: proxy,
42
+ skewed: true,
43
+ warning: `CLI ${cliVersion} does not match the running proxy ${proxy} — this ocx on PATH is stale. `
44
+ + "Its help and features describe a different build. Reinstall, or run the proxy's own binary.",
45
+ };
46
+ }
@@ -5,6 +5,27 @@ import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
5
5
 
6
6
  export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
7
7
 
8
+ /**
9
+ * Account log labels come in two families (#2699):
10
+ *
11
+ * - `p<hex6>` (plus the literal `main`) — a Codex pool account.
12
+ * - `o<hex6>` — a non-Codex OAuth provider account (xai, cursor, and siblings).
13
+ *
14
+ * Both are sha256-derived digests, never an email and never a raw provider account id. That is
15
+ * a privacy requirement, not a formatting preference: these labels are written to the usage log
16
+ * and served over the management API.
17
+ *
18
+ * hex6 is 16.7M values, so two accounts CAN collide and merge into one reported row. That is a
19
+ * reporting inaccuracy at operator scale, not a correctness or privacy failure, and it is the
20
+ * accepted cost of keeping the existing `p` format byte-compatible.
21
+ */
22
+ export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^o[a-f0-9]{6}$/;
23
+ export const ACCOUNT_LOG_LABEL_RE = /^(?:main|[po][a-f0-9]{6})$/;
24
+
25
+ export function oauthAccountLogLabel(accountId: string, provider = ""): string {
26
+ return `o${createHash("sha256").update(`${provider}\0${accountId}`).digest("hex").slice(0, 6)}`;
27
+ }
28
+
8
29
  export function createCodexAccountLogLabel(existingLabels: Iterable<string | undefined | null> = []): string {
9
30
  const used = new Set([...existingLabels].filter((value): value is string => !!value));
10
31
  for (let i = 0; i < 16; i++) {
@@ -46,6 +46,7 @@ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../
46
46
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
47
47
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
48
48
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
49
+ import { recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
49
50
  import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
50
51
  import {
51
52
  COMBO_NAMESPACE,
@@ -1330,6 +1331,9 @@ async function fetchProviderModelsWithAuth(
1330
1331
  });
1331
1332
  if (liveResult.ok) {
1332
1333
  const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
1334
+ // Live Max-Mode evidence feeds the umbrella resolver's ultra gate
1335
+ // (devlog 260828_cursor_umbrella_catalog; union with static evidence).
1336
+ recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? []);
1333
1337
  const result = available.length > 0 ? available : configured;
1334
1338
  // Cache the discovery-filtered roster without combo retention so a later
1335
1339
  // gather can re-apply the current capture's retain set on read.
@@ -280,7 +280,16 @@ export interface CodexCoordinatorAdoptionOptions {
280
280
  readonly onCheckpoint?: (checkpoint: CodexCoordinatorAdoptionCheckpoint) => void;
281
281
  }
282
282
 
283
- function fsyncPath(path: string): void {
283
+ function fsyncFile(path: string): void {
284
+ // Writable fd on purpose. Windows FlushFileBuffers needs GENERIC_WRITE;
285
+ // openSync(path, "r") is GENERIC_READ only and fails with EPERM (measured on
286
+ // Windows 11 25H2 / bun 1.3.14). prompt-journal.ts uses the same "r+" open.
287
+ const fd = openSync(path, "r+");
288
+ try { fsyncSync(fd); } finally { closeSync(fd); }
289
+ }
290
+
291
+ function fsyncDir(path: string): void {
292
+ // POSIX directory fsync is O_RDONLY. O_RDWR ("r+") is EISDIR.
284
293
  const fd = openSync(path, "r");
285
294
  try { fsyncSync(fd); } finally { closeSync(fd); }
286
295
  }
@@ -316,12 +325,12 @@ function publishAdoptionDatabase(
316
325
  } finally {
317
326
  temp.close();
318
327
  }
319
- fsyncPath(tempPath);
328
+ fsyncFile(tempPath);
320
329
  options.onCheckpoint?.("temp-committed");
321
330
 
322
331
  linkSync(tempPath, finalDatabasePath);
323
332
  options.onCheckpoint?.("published");
324
- if (process.platform !== "win32") fsyncPath(parent);
333
+ if (process.platform !== "win32") fsyncDir(parent);
325
334
  } catch (error) {
326
335
  if (errorCode(error) !== "EEXIST") throw error;
327
336
  } finally {
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
9
9
  export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
10
10
  schemaVersion: 1,
11
11
  id: "openai.codex-forward.gpt-5-6-sol.responses",
12
- version: "1.2.0",
12
+ version: "1.3.0",
13
13
  subject: {
14
14
  providerId: "openai",
15
15
  baseUrl: "https://chatgpt.com/backend-api/codex",
@@ -109,5 +109,13 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
109
109
  limitation: "The field is removed without inventing a replacement prompt_cache_options value.",
110
110
  evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-retention-removed"] }],
111
111
  },
112
+ {
113
+ id: "prompt-cache-options",
114
+ feature: "request.prompt_cache_options",
115
+ disposition: "unsupported",
116
+ summary: "The ChatGPT Codex forward route does not receive public prompt cache options.",
117
+ limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
118
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-options-removed"] }],
119
+ },
112
120
  ],
113
121
  } as const);