@iislee/opencodex 2.31.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
package/src/service.ts CHANGED
@@ -10,8 +10,8 @@ import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/
10
10
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { homedir, tmpdir } from "node:os";
12
12
  import { dirname, join, posix, resolve, win32 } from "node:path";
13
- import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
14
- import { loadConfig } from "./config";
13
+ import { expandUserPath, getConfigDir, loadConfig } from "./config";
14
+ import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config/process-state";
15
15
  import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
16
16
  import { stripGrokConfig } from "./grok/inject";
17
17
  import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
@@ -19,6 +19,7 @@ import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from
19
19
  import type { BunRuntimeSource } from "./lib/bun-runtime";
20
20
  import { isProcessAlive, stopProxy } from "./lib/process-control";
21
21
  import { serviceApiTokenFilePath } from "./lib/service-secrets";
22
+ import { tokenCollidesWithAdmin } from "./lib/admin-secrets";
22
23
  import { PROXY_ENV_KEYS } from "./lib/proxy-env";
23
24
  import { randomUUID } from "node:crypto";
24
25
  import {
@@ -97,13 +98,17 @@ function defaultOpenCodexHome(): string {
97
98
  return resolve(join(homedir(), ".opencodex"));
98
99
  }
99
100
 
100
- function serviceStatePaths(): string[] {
101
- const paths = [serviceStatePath()];
101
+ export function serviceStatePathsForOpenCodexHome(opencodexHome: string): string[] {
102
+ const paths = [join(opencodexHome, "service-state.json")];
102
103
  const defaultPath = join(defaultOpenCodexHome(), "service-state.json");
103
104
  if (normalizePathForCompare(defaultPath) !== normalizePathForCompare(paths[0])) paths.push(defaultPath);
104
105
  return paths;
105
106
  }
106
107
 
108
+ function serviceStatePaths(): string[] {
109
+ return serviceStatePathsForOpenCodexHome(currentOpenCodexHome());
110
+ }
111
+
107
112
  function currentCodexHome(deps: CodexHomeDeps = {}): string {
108
113
  // Service ownership must identify the same home as the runtime. In WSL an
109
114
  // unset CODEX_HOME can resolve to the single Windows Desktop home rather than
@@ -362,8 +367,38 @@ export function serviceRetryCommand(
362
367
  return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
363
368
  }
364
369
 
370
+ /**
371
+ * Refuse a management (admin) token as the data-plane secret.
372
+ *
373
+ * The service exports the contents of the service token file as
374
+ * `OPENCODEX_API_AUTH_TOKEN` before starting the proxy. When that value is the admin
375
+ * token, the server treats the management credential as a data-plane admission secret
376
+ * and fails the ENTIRE management plane closed at boot, so every `/api/*` request
377
+ * returns 503 — even on a loopback install that never needed a data-plane secret.
378
+ * Exporting the admin token in the CLI cannot recover it, because the fence is decided
379
+ * server-side at startup (#2696).
380
+ *
381
+ * Nothing in this codebase puts an admin token in that env var; it arrives from the
382
+ * installing shell. This function is the chokepoint that should refuse it rather than
383
+ * writing a file that produces a broken service. Comparison is the same helper doctor
384
+ * uses: minted `ocx_admin_…` prefix, or byte-equal to configuredAdminToken (env or file).
385
+ */
386
+ export function assertNotAdminToken(token: string, env: NodeJS.ProcessEnv = process.env): void {
387
+ if (!tokenCollidesWithAdmin(token, env)) return;
388
+ throw new Error(
389
+ "OPENCODEX_API_AUTH_TOKEN holds a management (admin) token. The service exports it "
390
+ + "as the data-plane secret, which fences the whole management API closed and makes "
391
+ + "every ocx management command fail with 503. Unset OPENCODEX_API_AUTH_TOKEN, or set "
392
+ + "it to a distinct data-plane key, then rerun the install.",
393
+ );
394
+ }
395
+
365
396
  export function assertServiceAuthEnvironment(): void {
366
397
  const config = loadConfig();
398
+ // Check the collision before the loopback short-circuit: a loopback install writes
399
+ // the token file too, so returning early here is what let the broken state through.
400
+ const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
401
+ if (present) assertNotAdminToken(present);
367
402
  if (isLoopbackHostname(config.hostname)) return;
368
403
  if (process.env.OPENCODEX_API_AUTH_TOKEN?.trim()) return;
369
404
  // Reached from `service repair` as well as `install`, so name a command that can
@@ -379,6 +414,9 @@ export function assertServiceAuthEnvironment(): void {
379
414
  function writeServiceApiTokenFile(): string | null {
380
415
  const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
381
416
  if (!token) return null;
417
+ // Last line of defence: every install/repair path funnels through here, so a
418
+ // collision cannot reach disk regardless of which caller ran (#2696).
419
+ assertNotAdminToken(token);
382
420
  const path = serviceApiTokenFilePath();
383
421
  const dir = getConfigDir();
384
422
  recordOwnedConfigPath(dir, path);
@@ -38,6 +38,7 @@ export interface PolicyJobOutcome {
38
38
  freedBytes?: number;
39
39
  removed?: number;
40
40
  trashDir?: string;
41
+ metadataPersistenceError?: PolicyRunResult["metadataPersistenceError"];
41
42
  }
42
43
 
43
44
  export interface PolicyJobState {
@@ -238,6 +239,7 @@ export async function abortStorageCleanupPolicyJobAsync(): Promise<void> {
238
239
  }
239
240
  }
240
241
 
242
+ /** Project a run result into the bounded management-API job outcome. */
241
243
  function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
242
244
  return {
243
245
  ok: result.ok,
@@ -248,18 +250,26 @@ function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
248
250
  ...(result.freedBytes !== undefined ? { freedBytes: result.freedBytes } : {}),
249
251
  ...(result.removed !== undefined ? { removed: result.removed } : {}),
250
252
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
253
+ ...(result.metadataPersistenceError
254
+ ? { metadataPersistenceError: result.metadataPersistenceError }
255
+ : {}),
251
256
  };
252
257
  }
253
258
 
259
+ /** Publish one completed evaluation without losing successful cleanup effects. */
254
260
  function applyFinished(result: PolicyRunResult): void {
255
261
  // Prefer the latest persisted policy over `result.policy`. The worker (or
256
262
  // in-process run) already merged run metadata into disk; a concurrent PUT
257
263
  // may also have landed after that write. Re-reading avoids applying a stale
258
264
  // start-of-job snapshot when the run skipped without saving.
259
- try {
260
- livePolicyApply?.(readStorageCleanupPolicyFromConfig());
261
- } catch {
262
- livePolicyApply?.(result.policy);
265
+ // A best-effort fallback policy may predate concurrent edits; keep the current
266
+ // live config untouched when the durable metadata write did not land.
267
+ if (!result.metadataPersistenceError) {
268
+ try {
269
+ livePolicyApply?.(readStorageCleanupPolicyFromConfig());
270
+ } catch {
271
+ livePolicyApply?.(result.policy);
272
+ }
263
273
  }
264
274
  state = {
265
275
  status: "idle",
@@ -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, saveConfigPreservingClaudeCode } from "../config";
12
12
  import type { StorageCleanupPolicy } from "../types";
13
13
  import {
14
14
  computePreviewDigest,
@@ -38,6 +38,7 @@ export function setStorageCleanupPolicyLiveSink(
38
38
 
39
39
  export type PolicySchedule = StorageCleanupPolicy["schedule"];
40
40
  export type PolicyRunReason = "startup" | "schedule" | "manual";
41
+ export type PolicyMetadataPersistenceError = "missing" | "invalid" | "conflict" | "write_failed";
41
42
 
42
43
  export type PolicySkipReason =
43
44
  | "disabled"
@@ -54,6 +55,7 @@ export interface PolicyRunResult {
54
55
  freedBytes?: number;
55
56
  removed?: number;
56
57
  trashDir?: string;
58
+ metadataPersistenceError?: PolicyMetadataPersistenceError;
57
59
  policy: StorageCleanupPolicy;
58
60
  }
59
61
 
@@ -399,6 +401,21 @@ export type PolicyRunMetadataPatch = {
399
401
  lastRun?: StorageCleanupPolicy["lastRun"];
400
402
  };
401
403
 
404
+ /** Apply only run-owned fields while preserving the supplied policy settings. */
405
+ function applyPolicyRunMetadata(
406
+ policy: StorageCleanupPolicy,
407
+ patch: PolicyRunMetadataPatch,
408
+ ): StorageCleanupPolicy {
409
+ let next =
410
+ patch.nextRun === "defer_busy"
411
+ ? deferBusy(policy, patch.now)
412
+ : advanceNextRun(policy, patch.now);
413
+ if (patch.lastRun) {
414
+ next = { ...next, lastRun: patch.lastRun };
415
+ }
416
+ return next;
417
+ }
418
+
402
419
  /**
403
420
  * Reload the latest persisted policy and write only run-owned metadata
404
421
  * (`lastRun` / `nextRun`). Preserves concurrent edits to enabled, trigger,
@@ -410,17 +427,57 @@ export function commitPolicyRunMetadata(
410
427
  patch: PolicyRunMetadataPatch,
411
428
  ): StorageCleanupPolicy {
412
429
  const latest = normalizeStorageCleanupPolicy(load());
413
- let next =
414
- patch.nextRun === "defer_busy"
415
- ? deferBusy(latest, patch.now)
416
- : advanceNextRun(latest, patch.now);
417
- if (patch.lastRun) {
418
- next = { ...next, lastRun: patch.lastRun };
419
- }
430
+ const next = applyPolicyRunMetadata(latest, patch);
420
431
  save(next);
421
432
  return next;
422
433
  }
423
434
 
435
+ type PolicyRunMetadataCommit = {
436
+ policy: StorageCleanupPolicy;
437
+ persistenceError?: PolicyMetadataPersistenceError;
438
+ };
439
+
440
+ /** Attach the durable metadata outcome without replacing cleanup status or metrics. */
441
+ function withMetadataCommit(
442
+ result: Omit<PolicyRunResult, "policy" | "metadataPersistenceError">,
443
+ committed: PolicyRunMetadataCommit,
444
+ ): PolicyRunResult {
445
+ return {
446
+ ...result,
447
+ policy: committed.policy,
448
+ ...(committed.persistenceError ? { metadataPersistenceError: committed.persistenceError } : {}),
449
+ };
450
+ }
451
+
452
+ /** Recompute run-owned metadata from the latest config inside the mutation lock. */
453
+ function commitPolicyRunMetadataToConfig(
454
+ patch: PolicyRunMetadataPatch,
455
+ fallbackPolicy: StorageCleanupPolicy,
456
+ ): PolicyRunMetadataCommit {
457
+ const unavailable = (reason: PolicyMetadataPersistenceError): PolicyRunMetadataCommit => {
458
+ console.warn(`[storage-policy] metadata_persist_failed reason=${reason}`);
459
+ return {
460
+ policy: applyPolicyRunMetadata(fallbackPolicy, patch),
461
+ persistenceError: reason,
462
+ };
463
+ };
464
+ try {
465
+ const outcome = mutatePersistedConfig(config => {
466
+ const next = applyPolicyRunMetadata(
467
+ normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
468
+ patch,
469
+ );
470
+ config.storageCleanupPolicy = next;
471
+ return { changed: true, value: next };
472
+ });
473
+ if (outcome.status === "unavailable") return unavailable(outcome.reason);
474
+ livePolicySink?.(outcome.value);
475
+ return { policy: outcome.value };
476
+ } catch {
477
+ return unavailable("write_failed");
478
+ }
479
+ }
480
+
424
481
  function logPolicyEvent(message: string): void {
425
482
  console.log(`[storage-policy] ${message}`);
426
483
  }
@@ -434,8 +491,14 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
434
491
  const load = deps.loadPolicy ?? readStorageCleanupPolicyFromConfig;
435
492
  const save = deps.savePolicy ?? writeStorageCleanupPolicyToConfig;
436
493
  const execute = deps.execute ?? executeArchivedCleanup;
437
-
438
494
  const policy = normalizeStorageCleanupPolicy(load());
495
+ // Injected stores retain the existing load/save contract. The production path
496
+ // recomputes metadata from the latest persisted policy inside the config lock.
497
+ const commitMetadata = deps.loadPolicy !== undefined || deps.savePolicy !== undefined
498
+ ? (patch: PolicyRunMetadataPatch): PolicyRunMetadataCommit => ({
499
+ policy: commitPolicyRunMetadata(load, save, patch),
500
+ })
501
+ : (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadataToConfig(patch, policy);
439
502
 
440
503
  if (typeof deps.holdAfterLoadMs === "number" && Number.isFinite(deps.holdAfterLoadMs) && deps.holdAfterLoadMs > 0) {
441
504
  Bun.sleepSync(Math.floor(deps.holdAfterLoadMs));
@@ -451,15 +514,15 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
451
514
 
452
515
  const selection = selectPolicyPreview(policy, deps.codexHome);
453
516
  if (selection.archivedBytes <= policy.trigger.archivedBytesOver) {
454
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
517
+ const committed = commitMetadata({ now, nextRun: "advance" });
455
518
  logPolicyEvent("skip under_threshold");
456
- return { ok: true, skipped: "under_threshold", policy: saved };
519
+ return withMetadataCommit({ ok: true, skipped: "under_threshold" }, committed);
457
520
  }
458
521
 
459
522
  if (selection.count === 0) {
460
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
523
+ const committed = commitMetadata({ now, nextRun: "advance" });
461
524
  logPolicyEvent("skip nothing_selected");
462
- return { ok: true, skipped: "nothing_selected", policy: saved };
525
+ return withMetadataCommit({ ok: true, skipped: "nothing_selected" }, committed);
463
526
  }
464
527
 
465
528
  const result = execute({
@@ -473,25 +536,28 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
473
536
  });
474
537
 
475
538
  if (!result.ok && result.error === "codex_busy") {
476
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "defer_busy" });
539
+ const committed = commitMetadata({ now, nextRun: "defer_busy" });
477
540
  logPolicyEvent("defer codex_busy");
478
- return { ok: false, deferred: "codex_busy", error: "codex_busy", policy: saved };
541
+ return withMetadataCommit({
542
+ ok: false,
543
+ deferred: "codex_busy",
544
+ error: "codex_busy",
545
+ }, committed);
479
546
  }
480
547
 
481
548
  if (!result.ok) {
482
549
  // Non-busy failure: still advance schedule so we do not tight-loop.
483
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
550
+ const committed = commitMetadata({ now, nextRun: "advance" });
484
551
  logPolicyEvent(`fail ${result.error ?? "cleanup_failed"}`);
485
- return {
552
+ return withMetadataCommit({
486
553
  ok: false,
487
554
  error: result.error,
488
555
  mode: result.mode,
489
556
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
490
- policy: saved,
491
- };
557
+ }, committed);
492
558
  }
493
559
 
494
- const saved = commitPolicyRunMetadata(load, save, {
560
+ const committed = commitMetadata({
495
561
  now,
496
562
  nextRun: "advance",
497
563
  lastRun: {
@@ -503,14 +569,13 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
503
569
  logPolicyEvent(
504
570
  `ok mode=${result.mode} removed=${result.count} freedBytes=${result.bytes}`,
505
571
  );
506
- return {
572
+ return withMetadataCommit({
507
573
  ok: true,
508
574
  mode: result.mode,
509
575
  freedBytes: result.bytes,
510
576
  removed: result.count,
511
577
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
512
- policy: saved,
513
- };
578
+ }, committed);
514
579
  }
515
580
 
516
581
  /** Startup / schedule tick entry — swallows unexpected errors. */
@@ -239,14 +239,48 @@ export interface OcxClientIntegrationsConfig {
239
239
  "claude-desktop"?: boolean;
240
240
  }
241
241
 
242
+ export interface OcxConfigRebaseProvenance {
243
+ version: 1;
244
+ deletedTopLevelKeys: string[];
245
+ }
246
+
242
247
  export interface OcxConfig {
243
248
  port: number;
244
249
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
245
250
  emptyCompletionRetry?: boolean;
251
+ /**
252
+ * Whether a login may open a browser on the machine running the proxy.
253
+ *
254
+ * Absent and `true` both mean "open", which is what every existing install
255
+ * already does. Only an explicit `false` declines — for an operator who wants
256
+ * to paste the authorization URL into a different browser profile, or who is
257
+ * driving the dashboard from a different machine than the proxy.
258
+ *
259
+ * Deliberately a boolean and not an "auto" mode: inferring headlessness from
260
+ * SSH_CONNECTION or a missing DISPLAY breaks a working login silently when
261
+ * the guess is wrong.
262
+ */
263
+ oauthOpenBrowser?: boolean;
246
264
  /** Maximum usage-log bytes read for one management snapshot. */
247
265
  managementUsageMaxReadBytes?: number;
248
266
  providers: Record<string, OcxProviderConfig>;
249
267
  defaultProvider: string;
268
+ /** Persisted state for newly discovered provider models (#2464). Absent keeps legacy "on" behavior. */
269
+ modelDiscovery?: {
270
+ newModelPolicy?: "on" | "off";
271
+ knownModels?: Record<string, {
272
+ ids: string[];
273
+ removed: string[];
274
+ updatedAt: string;
275
+ /** Consecutive successful discoveries in which an active id was absent. */
276
+ missing?: Record<string, number>;
277
+ }>;
278
+ recentArrivals?: Record<string, Array<{ id: string; at: string }>>;
279
+ };
280
+ /** Enable the shipped model alias patterns for providers without an override. */
281
+ defaultModelAliases?: boolean;
282
+ /** Explicit top-level deletion intent used by stale whole-config rebases. */
283
+ configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
250
284
  /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
251
285
  openaiProviderTierVersion?: 1 | 2;
252
286
  /** One-time migration marker for Antigravity's static-catalog defaults. */
@@ -470,6 +504,13 @@ export interface OcxConfig {
470
504
  * those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
471
505
  */
472
506
  proxy?: string;
507
+ /**
508
+ * Hosts that bypass `proxy` for OpenCodex's own outbound provider calls, merged into
509
+ * NO_PROXY at startup. Accepts a comma-separated string (NO_PROXY syntax) or an array.
510
+ * Loopback is always excluded regardless of this setting, and an inherited NO_PROXY is
511
+ * preserved — this ADDS entries, it never replaces the environment.
512
+ */
513
+ noProxy?: string | string[];
473
514
  /**
474
515
  * Upstream stall timeout (seconds). After this many seconds of no upstream data, emits
475
516
  * response.incomplete. Default 300. Min 1.
@@ -541,6 +582,15 @@ export interface OcxConfig {
541
582
  * selector map remains visible for compatibility with hand-written configurations.
542
583
  */
543
584
  codexAccountPickerEnabled?: boolean;
585
+ /**
586
+ * Show the GPT-5.3-Codex-Spark weekly window on Codex quota surfaces. Default false.
587
+ *
588
+ * Spark is a single-model window that reads 0% for most operators, and on a multi-account
589
+ * pool it doubles the bar count for information almost nobody acts on. Hidden by default and
590
+ * revealed by an explicit `true`; a malformed value reads as hidden rather than rejecting the
591
+ * whole config.
592
+ */
593
+ showCodexSparkQuota?: boolean;
544
594
  /** Active pool account id for next session. undefined = main (passthrough as-is). */
545
595
  activeCodexAccountId?: string;
546
596
  /** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
@@ -570,6 +620,22 @@ export interface OcxConfig {
570
620
  /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
571
621
  stickyLimit?: number;
572
622
  };
623
+ /**
624
+ * Generic OAuth multi-account 429 failover (#2568). Presence-driven by default.
625
+ *
626
+ * Rotates to another logged-in account of the SAME provider when one is rate-limited, for
627
+ * OAuth providers that have no pool of their own — xAI, Cursor, Kimi, GitHub Copilot,
628
+ * Antigravity, Nous. The Codex pool and the Anthropic pool own their own rotation and are
629
+ * excluded; this setting changes neither.
630
+ *
631
+ * With the key absent, rotation activates when a provider has 2 or more eligible stored
632
+ * accounts — the same consent rule API-key pools already apply to a 2+ key pool (#2568d). A
633
+ * single account is a strict no-op. Set `false` to keep strict single-account behaviour;
634
+ * `providers.<name>.oauthAccountFailover` overrides this per provider.
635
+ */
636
+ oauthAccountFailover?: {
637
+ enabled?: boolean;
638
+ };
573
639
  /** Virtual `combo/<id>` models spanning concrete provider/model targets (issue #133). */
574
640
  combos?: Record<string, OcxComboConfig>;
575
641
  /**
@@ -115,6 +115,15 @@ export interface TierObservationContext {
115
115
  fastWire: FastWire | null;
116
116
  demandDecision: "force-fast" | "force-default" | "inherit";
117
117
  callerTier?: string;
118
+ /**
119
+ * Whether the destination's echoed `service_tier` is authoritative about Fast scheduling.
120
+ *
121
+ * The ChatGPT-internal Codex backend returns `service_tier: "default"` on turns that were in
122
+ * fact scheduled as priority, so treating its echo as a downgrade produced a false
123
+ * `response-declined` on every Fast request (#2558). Absent means "assume authoritative",
124
+ * preserving the behaviour for the public API where the echo does mean what it says.
125
+ */
126
+ responseTierAuthoritative?: boolean;
118
127
  }
119
128
 
120
129
  export type TierDecision =
@@ -127,6 +136,12 @@ export type TierDecision =
127
136
  * retries are allowed; OAuth/forward credentials and local runtimes are never replayed.
128
137
  */
129
138
  export interface OcxProviderConfig {
139
+ /** Optional short provider namespace used only at request/catalog presentation time. */
140
+ alias?: string;
141
+ /** Native model id -> short, slash-free request alias. */
142
+ modelAliases?: Record<string, string>;
143
+ /** Override the global built-in model-alias switch for this provider. */
144
+ defaultAliases?: boolean;
130
145
  adapter: string;
131
146
  /**
132
147
  * Codex tool calling mode for routed models.
@@ -266,6 +281,33 @@ export interface OcxProviderConfig {
266
281
  * full set so the user can pick). See devlog issue_052_provider-model-allowlist.
267
282
  */
268
283
  selectedModels?: string[];
284
+ /** Override for newly discovered models. Absent/"inherit" uses the install policy. */
285
+ newModelPolicy?: "on" | "off" | "inherit";
286
+ /**
287
+ * Model-preset marker for `selectedModels` (#2465). Absent means "all", exactly today's
288
+ * semantics — an existing provider is never narrowed by an upgrade.
289
+ *
290
+ * The preset is a SEED, not a lock: `selectedModels` holds concrete ids materialized from
291
+ * the shipped rules, so every existing consumer and older binaries keep working against a
292
+ * plain allowlist. Divergence is detected at the WRITE path rather than by diffing — any user
293
+ * edit while the mode is "preset" flips it to "custom", after which the proxy never
294
+ * re-materializes. That collapses upgrade reconciliation to a version compare.
295
+ *
296
+ * Deliberately distinct from `deriveProviderPresets`, which curates WHICH PROVIDERS to offer.
297
+ * This curates which MODELS a provider exposes; the code says "model preset" throughout.
298
+ */
299
+ modelPreset?: {
300
+ mode: "preset" | "all" | "custom";
301
+ /** MODEL_PRESETS version materialized into `selectedModels`. */
302
+ appliedVersion?: number;
303
+ appliedAt?: string;
304
+ /**
305
+ * Set when materialization matched nothing and the provider fell back to "all". A preset
306
+ * must never write an empty allowlist, because empty means ALL and would silently
307
+ * un-curate; the fallback marker lets the next convergence retry.
308
+ */
309
+ fallback?: "preset-empty";
310
+ };
269
311
  /** Provider-wide fallback when context metadata is absent; otherwise caps the reported window. */
270
312
  contextWindow?: number;
271
313
  /** Per-model fallback when context metadata is absent; otherwise caps the reported window. */
@@ -274,6 +316,11 @@ export interface OcxProviderConfig {
274
316
  modelInputModalities?: Record<string, string[]>;
275
317
  /** Model-specific max input token limits. Values cap auto_compact_token_limit. */
276
318
  modelMaxInputTokens?: Record<string, number>;
319
+ /**
320
+ * Per-model soft compaction budgets. Values may only lower the effective
321
+ * context/max-input envelope; they never raise hard admission limits.
322
+ */
323
+ modelAutoCompactTokenLimits?: Record<string, number>;
277
324
  /**
278
325
  * Provider-wide fallback for chat-completions `max_tokens` when the caller omits
279
326
  * Responses `max_output_tokens`. Adapters still let an explicit request win.
@@ -304,6 +351,16 @@ export interface OcxProviderConfig {
304
351
  * providers whose registry entry declares authKind "local" (management API enforces).
305
352
  */
306
353
  authMode?: "key" | "forward" | "oauth" | "local";
354
+ /**
355
+ * Per-provider override for generic OAuth multi-account 429 failover (#2568).
356
+ *
357
+ * Rotation is presence-driven by default — 2+ logged-in accounts activate it — so this exists
358
+ * for the operator who accepts rotation on one provider and refuses it on another. An explicit
359
+ * boolean here beats the global `oauthAccountFailover` and beats presence.
360
+ */
361
+ oauthAccountFailover?: {
362
+ enabled?: boolean;
363
+ };
307
364
  /** Allow an explicitly key/oauth provider to run without a credential (for keyless local proxies). */
308
365
  keyOptional?: boolean;
309
366
  /**
@@ -336,6 +393,18 @@ export interface OcxProviderConfig {
336
393
  * Responses backend rejects Codex summary-delivery fields for that model.
337
394
  */
338
395
  modelSupportsReasoningSummaries?: Record<string, boolean>;
396
+ /**
397
+ * Model-specific Codex Responses verbosity capability. Set false when the upstream ignores
398
+ * `text.verbosity`; the catalog hides the no-op picker and the Responses adapter strips stale
399
+ * or caller-supplied values while preserving other `text` fields.
400
+ */
401
+ modelSupportsVerbosity?: Record<string, boolean>;
402
+ /**
403
+ * Provider-wide Codex Responses verbosity capability, applied to models the per-model map
404
+ * does not enumerate (a live-discovered id, for example). Materialized from the registry at
405
+ * seed/enrich time so the catalog hint pass never has to read PROVIDER_REGISTRY.
406
+ */
407
+ supportsVerbosity?: boolean;
339
408
  /**
340
409
  * Per-model wire value for Responses `stream_options.reasoning_summary_delivery`.
341
410
  * Presence also advertises reasoning-summary support for that routed model.
@@ -30,6 +30,11 @@ export interface OcxReasoningReplayIdentity {
30
30
  * the holder, so late tool-call cache writes see the active physical identity.
31
31
  */
32
32
  export interface OcxReasoningReplayScopeRef {
33
+ /**
34
+ * Conversation namespace for replay state. Historically this was always the Codex parent-thread
35
+ * id; headerless Responses callers use a raw sanitized thread/Cursor/session fallback, never the
36
+ * hashed request-log conversation id.
37
+ */
33
38
  readonly clientThreadId: string;
34
39
  current?: Readonly<OcxReasoningReplayIdentity>;
35
40
  }
@@ -63,7 +68,9 @@ export interface OcxParsedRequest {
63
68
  _cursorConversationId?: string;
64
69
  /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
65
70
  _clientThreadId?: string;
66
- /** Provider/account/model-bound namespace for process-local raw-reasoning replay. */
71
+ /** Cursor-only thread owner; may be an opaque process-local Desktop session/thread identity. */
72
+ _cursorClientThreadId?: string;
73
+ /** Conversation/provider/account/model-bound namespace for reasoning replay state. */
67
74
  _reasoningReplayScope?: OcxReasoningReplayScopeRef;
68
75
  /**
69
76
  * Set by bindRouteReasoningReplayScope after a proven serving-identity change, or by
@@ -81,7 +88,7 @@ export interface OcxParsedRequest {
81
88
  */
82
89
  _cursorIsolateConversation?: boolean;
83
90
  /** Account-scoped, non-secret Kiro request metadata selected with the OAuth access token. */
84
- _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
91
+ _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
85
92
  /** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
86
93
  _providerContinuation?: OcxProviderContinuationState;
87
94
  /** Persisted continuation considered only after the final physical route is known. */
@@ -183,8 +190,14 @@ export interface OcxImageContent {
183
190
  detail?: string;
184
191
  }
185
192
 
186
- /** A user/developer message content part: text or an image (vision). */
187
- export type OcxContentPart = OcxTextContent | OcxImageContent;
193
+ export interface OcxVideoContent {
194
+ type: "video";
195
+ /** A base64 `data:` URL from an OpenAI-compatible `video_url` part. */
196
+ videoUrl: string;
197
+ }
198
+
199
+ /** A user/developer message content part: text or native media. */
200
+ export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent;
188
201
 
189
202
  export interface OcxThinkingContent {
190
203
  type: "thinking";