@bitkyc08/opencodex 2.75.0 → 2.76.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 (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -1,13 +1,17 @@
1
- import { lstatSync, readdirSync, realpathSync } from "node:fs";
1
+ import { lstatSync, readdirSync, realpathSync, statSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
- import { isAbsolute, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { npmInvocation } from "./npm-invocation.mjs";
6
7
 
7
8
  const WORKER_ARG = "--ocx-npm-cache-preflight-worker";
9
+ const ROOT_ONLY_ARG = "--root-only";
8
10
  const PROTOCOL_VERSION = 1;
9
- const WORKER_TIMEOUT_MS = 10_000;
10
- const NPM_CONFIG_TIMEOUT_MS = 5_000;
11
+ // Windows starts npm through cmd.exe and is routinely slowed by on-access scanning, so the
12
+ // same `npm config get cache` that takes well under a second on POSIX can take several there.
13
+ const WORKER_TIMEOUT_MS = process.platform === "win32" ? 25_000 : 10_000;
14
+ const NPM_CONFIG_TIMEOUT_MS = process.platform === "win32" ? 15_000 : 5_000;
11
15
  const INSPECTION_TIMEOUT_MS = 7_500;
12
16
  const MAX_ENTRIES = 100_000;
13
17
  const MAX_DEPTH = 64;
@@ -17,6 +21,8 @@ const RESULT_REASONS = new Set([
17
21
  "cache_entry_foreign_owner",
18
22
  "cache_entry_inaccessible",
19
23
  "cache_path_malformed",
24
+ "cache_root_dangling_link",
25
+ "cache_root_not_directory",
20
26
  "inspection_incomplete",
21
27
  "npm_config_failed",
22
28
  "npm_unavailable",
@@ -29,12 +35,58 @@ function inaccessibleByMode(stat) {
29
35
  return (ownerBits & 0o400) === 0;
30
36
  }
31
37
 
38
+ /**
39
+ * Check that npm can create or use its cache ROOT (#6288). This is the part of the inspection
40
+ * that is meaningful on every platform: npm's first action is `mkdir -p <cache>`, and it fails
41
+ * with ENOTDIR when the root — or the nearest existing ancestor npm would create it under — is
42
+ * a file, or a link/junction whose target is gone. A Windows directory junction relocated to
43
+ * another volume is ordinary configuration; only an unresolvable one is a problem.
44
+ */
45
+ export function inspectNpmCacheRoot(cachePath, options = {}) {
46
+ const lstat = options.lstatFn ?? lstatSync;
47
+ const stat = options.statFn ?? statSync;
48
+ let current = resolve(cachePath);
49
+ let entry;
50
+ for (;;) {
51
+ try {
52
+ entry = lstat(current);
53
+ break;
54
+ } catch (error) {
55
+ // A file somewhere above the root: POSIX reports ENOTDIR here, which is exactly the
56
+ // ENOTDIR npm's own mkdir would hit. Windows reports ENOENT and the walk reaches the file.
57
+ if (error?.code === "ENOTDIR") return { ok: false, reason: "cache_root_not_directory" };
58
+ if (error?.code !== "ENOENT") return { ok: false, reason: "cache_entry_inaccessible" };
59
+ const parent = dirname(current);
60
+ // Nothing on the path exists at all (a missing drive root); npm cannot mkdir there either.
61
+ if (parent === current) return { ok: false, reason: "cache_root_not_directory" };
62
+ current = parent;
63
+ }
64
+ }
65
+ let target = entry;
66
+ if (entry.isSymbolicLink()) {
67
+ try {
68
+ target = stat(current);
69
+ } catch (error) {
70
+ return {
71
+ ok: false,
72
+ reason: error?.code === "ENOENT" || error?.code === "ENOTDIR" || error?.code === "ELOOP"
73
+ ? "cache_root_dangling_link"
74
+ : "cache_entry_inaccessible",
75
+ };
76
+ }
77
+ }
78
+ if (!target.isDirectory()) return { ok: false, reason: "cache_root_not_directory" };
79
+ return { ok: true, reason: "cache_accessible" };
80
+ }
81
+
32
82
  /**
33
83
  * Inspect an existing Unix npm cache without following symlinks. The limits are
34
84
  * deliberately part of the result contract: an incomplete inspection cannot prove
35
85
  * that replacing the live package will succeed.
36
86
  */
37
87
  export function inspectNpmCacheDirectory(cachePath, options = {}) {
88
+ const root = inspectNpmCacheRoot(cachePath, options);
89
+ if (!root.ok) return root;
38
90
  const expectedUid = options.expectedUid ?? process.getuid?.();
39
91
  const deadline = (options.nowMs ?? Date.now)() + (options.timeoutMs ?? INSPECTION_TIMEOUT_MS);
40
92
  const maxEntries = options.maxEntries ?? MAX_ENTRIES;
@@ -118,22 +170,59 @@ export function inspectNpmCacheDirectory(cachePath, options = {}) {
118
170
  return { ok: true, reason: "cache_accessible" };
119
171
  }
120
172
 
121
- function workerResult() {
122
- const invocation = npmInvocation(["config", "get", "cache"]);
173
+ // cmd.exe re-parses npm.cmd's %* after our quoting, so on Windows a pinned path must not carry
174
+ // characters that quoting cannot neutralize there. Such a path is refused, never escaped.
175
+ const WINDOWS_CMD_METACHARACTERS = /["%!^&|<>]/;
176
+
177
+ function validCachePath(value, platform = process.platform) {
178
+ return typeof value === "string" && value.length > 0 && value.length <= 4096
179
+ && !value.includes("\0") && !/[\r\n]/.test(value) && isAbsolute(value)
180
+ && !(platform === "win32" && WINDOWS_CMD_METACHARACTERS.test(value));
181
+ }
182
+
183
+ /**
184
+ * Resolve the cache npm itself uses, with the operator's full npmrc chain. The transactional
185
+ * stage pins this path with `--cache`: its `--prefix <stage>` moves npm's globalconfig to
186
+ * `<stage>/etc/npmrc`, so without the pin a `cache=` from the global npmrc is silently dropped
187
+ * and staging falls back to npm's default root — not the root this preflight checked (#6288).
188
+ */
189
+ export function resolveNpmCachePath(options = {}) {
190
+ const env = options.env ?? process.env;
191
+ const platform = options.platform ?? process.platform;
192
+ // Global mode, from the home directory: the staged install is `npm install -g`, which never
193
+ // reads a project .npmrc, so a repository the user happens to run `ocx update` in must not be
194
+ // able to choose the cache that install is pinned to.
195
+ const invocation = (options.invocationFn ?? npmInvocation)(["config", "get", "cache", "--global"], platform, env);
123
196
  if (!invocation) return { ok: false, reason: "npm_unavailable" };
124
- const npm = spawnSync(invocation.file, invocation.args, {
197
+ const npm = (options.spawnSyncFn ?? spawnSync)(invocation.file, invocation.args, {
125
198
  encoding: "utf8",
126
- timeout: NPM_CONFIG_TIMEOUT_MS,
199
+ timeout: options.timeoutMs ?? NPM_CONFIG_TIMEOUT_MS,
127
200
  windowsHide: true,
201
+ cwd: options.cwd ?? homedir(),
128
202
  ...invocation.options,
203
+ env: invocation.options?.env ?? env,
129
204
  });
130
- if (npm.status !== 0) return { ok: false, reason: "npm_config_failed" };
131
-
205
+ if (npm?.status !== 0) return { ok: false, reason: "npm_config_failed" };
132
206
  const output = typeof npm.stdout === "string" ? npm.stdout.trim() : "";
133
- if (!output || output.length > 4096 || output.includes("\0") || /[\r\n]/.test(output) || !isAbsolute(output)) {
134
- return { ok: false, reason: "cache_path_malformed" };
207
+ if (!validCachePath(output, platform)) return { ok: false, reason: "cache_path_malformed" };
208
+ return { ok: true, path: output };
209
+ }
210
+
211
+ function workerResult(argv) {
212
+ const rootOnly = argv.includes(ROOT_ONLY_ARG);
213
+ const supplied = argv.find(arg => arg !== ROOT_ONLY_ARG);
214
+ let cachePath;
215
+ if (supplied !== undefined) {
216
+ if (!validCachePath(supplied)) return { ok: false, reason: "cache_path_malformed" };
217
+ cachePath = supplied;
218
+ } else {
219
+ const resolved = resolveNpmCachePath();
220
+ if (!resolved.ok) return resolved;
221
+ cachePath = resolved.path;
135
222
  }
136
- return inspectNpmCacheDirectory(output);
223
+ // Windows has no uid and no Unix owner bits, so the deep ownership/mode walk proves nothing
224
+ // there; the root check is the part of the contract that holds on every platform.
225
+ return rootOnly ? inspectNpmCacheRoot(cachePath) : inspectNpmCacheDirectory(cachePath);
137
226
  }
138
227
 
139
228
  // Reasons that legitimately accompany `ok: true`. The parser below cross-checks the flag against
@@ -161,15 +250,33 @@ function parseWorkerOutput(stdout) {
161
250
  }
162
251
  }
163
252
 
164
- /** Run the bounded cache inspection in an isolated, synchronously-timeboxed worker. */
253
+ /**
254
+ * Run the bounded cache inspection in an isolated, synchronously-timeboxed worker. Pass
255
+ * `cachePath` (from {@link resolveNpmCachePath}) to inspect exactly the root a later stage pins;
256
+ * without it the worker resolves npm's cache itself.
257
+ */
165
258
  export function runNpmCachePreflight(options = {}) {
166
- if ((options.platform ?? process.platform) === "win32") {
259
+ const result = runPreflightWorker(options);
260
+ // Before #6288 Windows skipped this gate entirely. Only the root shapes that make npm's own
261
+ // mkdir fail with ENOTDIR may now refuse an update there; a slow or unavailable npm, a worker
262
+ // timeout, or any other inconclusive result keeps the previous behavior instead of blocking.
263
+ if ((options.platform ?? process.platform) === "win32" && !result.ok
264
+ && !WINDOWS_BLOCKING_REASONS.has(result.reason)) {
167
265
  return { ok: true, reason: "windows_skip" };
168
266
  }
267
+ return result;
268
+ }
269
+
270
+ const WINDOWS_BLOCKING_REASONS = new Set(["cache_root_dangling_link", "cache_root_not_directory"]);
271
+
272
+ function runPreflightWorker(options) {
273
+ const workerArgs = [fileURLToPath(import.meta.url), WORKER_ARG];
274
+ if ((options.platform ?? process.platform) === "win32") workerArgs.push(ROOT_ONLY_ARG);
275
+ if (options.cachePath !== undefined) workerArgs.push(options.cachePath);
169
276
  const spawn = options.spawnSyncFn ?? spawnSync;
170
277
  const result = spawn(
171
278
  options.execPath ?? process.execPath,
172
- [fileURLToPath(import.meta.url), WORKER_ARG],
279
+ workerArgs,
173
280
  {
174
281
  encoding: "utf8",
175
282
  timeout: options.timeoutMs ?? WORKER_TIMEOUT_MS,
@@ -182,8 +289,14 @@ export function runNpmCachePreflight(options = {}) {
182
289
  return parseWorkerOutput(result.stdout) ?? { ok: false, reason: "worker_output_malformed" };
183
290
  }
184
291
 
185
- /** Fixed operator guidance; worker/npm output is intentionally never interpolated. */
292
+ /** Fixed operator guidance; worker/npm output and the cache path are intentionally never interpolated. */
186
293
  export function npmCachePreflightFailureMessage(reason) {
294
+ if (reason === "cache_root_dangling_link") {
295
+ return `npm cache access pre-flight failed (${reason}); npm's cache folder (see 'npm config get cache') is a link or junction whose target is missing — recreate the target folder or remove the link, then retry`;
296
+ }
297
+ if (reason === "cache_root_not_directory") {
298
+ return `npm cache access pre-flight failed (${reason}); npm's cache folder (see 'npm config get cache') or a folder above it is a file or an unavailable drive, so npm would fail with ENOTDIR on mkdir — fix that path or set a different cache, then retry`;
299
+ }
187
300
  return `npm cache access pre-flight failed (${reason}); fix cache ownership and permissions, then retry`;
188
301
  }
189
302
 
@@ -193,7 +306,7 @@ const isWorker = process.argv[1]
193
306
  if (isWorker) {
194
307
  let result;
195
308
  try {
196
- result = workerResult();
309
+ result = workerResult(process.argv.slice(3));
197
310
  } catch {
198
311
  result = { ok: false, reason: "cache_entry_inaccessible" };
199
312
  }
@@ -9,19 +9,37 @@ type Environment = Record<string, string | undefined>;
9
9
  export interface UpdateOwnershipTransaction {
10
10
  controlEnvironment(environment?: Environment): Environment;
11
11
  unprivilegedEnvironment(environment?: Environment): Environment;
12
+ /**
13
+ * Free the lease before a service-manager-mediated start that cannot join it —
14
+ * the manager's `ocx start` child runs with the stored registration environment,
15
+ * not a delegated token, so held through the repair's serving wait the lease
16
+ * keeps that proxy from starting (#5760). Idempotent; controlEnvironment keeps
17
+ * injecting the token afterwards, which a released lease self-corrects (the
18
+ * delegated join fails and the child acquires normally).
19
+ */
20
+ release(): void;
21
+ /**
22
+ * Take the lease back before re-reading ownership and mutating directly: the
23
+ * released refresh window could have admitted a claim that must be vetoed, not
24
+ * killed unleased. No-op while held; a still-claimed lease throws (bounded),
25
+ * which callers must treat as fail-closed.
26
+ */
27
+ reacquire(): void;
12
28
  }
13
29
 
14
30
  /** One lease spans awaited stop, replacement and recovery. Never publish its token globally. */
15
31
  export async function withUpdateOwnershipLease<T>(
16
32
  paths: string[], run: (transaction: UpdateOwnershipTransaction) => Promise<T>,
17
33
  ): Promise<T> {
18
- const lease = acquireOwnershipMutationLease(paths);
34
+ let lease = acquireOwnershipMutationLease(paths);
19
35
  let released = false;
20
36
  const release = () => { if (!released) { released = true; lease.release(); } };
21
37
  try {
22
38
  return await run({
23
39
  controlEnvironment: (env = process.env) => ownershipMutationLeaseChildEnvironment(env, lease.token),
24
40
  unprivilegedEnvironment: (env = process.env) => unprivilegedOwnershipMutationEnvironment(env),
41
+ release: () => release(),
42
+ reacquire: () => { if (released) { lease = acquireOwnershipMutationLease(paths); released = false; } },
25
43
  });
26
44
  } finally { release(); }
27
45
  }
@@ -36,19 +36,93 @@ export function updateRestartVeto(
36
36
  return plan.notice ?? "The background runtime is owned elsewhere; it was left running.";
37
37
  }
38
38
 
39
+ /**
40
+ * The lease boundary inside a running restart, handed to `restart`.
41
+ *
42
+ * `ocx service repair` re-activates the OS service manager — Task Scheduler, launchd
43
+ * or systemd — and that `ocx start` child is not a descendant of this process: it runs
44
+ * with the stored registration environment, carries no delegated token, and can never
45
+ * join the lease held here. Held through the repair's serving wait, the lease keeps
46
+ * that child from ever starting (#5760; the npm lane releases at this boundary). The
47
+ * veto is the decision the lease must cover — the reclaim and kills it authorizes —
48
+ * so the fallthrough takes the lease back before re-running it: anything between the
49
+ * service refresh and the direct start would otherwise mutate the port unleased.
50
+ */
51
+ export interface UpdateRestartLeaseControl {
52
+ /** Free the lease before a service-manager-mediated start that cannot join it. Idempotent. */
53
+ releaseForServiceManager(): void;
54
+ /**
55
+ * Take the lease back before the fallthrough mutates the port, then re-run the veto
56
+ * under it. A lease that stays claimed (a live claimant mid-mutation) fails closed:
57
+ * the returned notice stops the restart before any kill or start, and `failed` marks
58
+ * the job failed — the refresh just before this did not produce a serving proxy, so
59
+ * nothing is known to be running. An ownership veto is not a failure.
60
+ */
61
+ reacquireForDirectStart(): UpdateRestartStop | null;
62
+ /** Re-run the recorded-owner veto; a non-null notice must stop the restart there. */
63
+ vetoAgain(): string | null;
64
+ }
65
+
66
+ /** Why the direct-start fallthrough stopped; `failed` means no proxy is known to be serving. */
67
+ export interface UpdateRestartStop {
68
+ readonly notice: string;
69
+ readonly failed: boolean;
70
+ }
71
+
72
+ /**
73
+ * Long enough to outlast one service-wrapper respawn cycle (5 s) of the managed child the
74
+ * refresh just started, which briefly holds the lease while it binds and publishes.
75
+ */
76
+ const REACQUIRE_WAIT_MS = 10_000;
77
+
39
78
  export async function runUpdateRestartWithOwnershipLease<T>(
40
79
  resolve: (() => ServiceOwnershipResolution) | undefined,
41
- restart: () => Promise<T>,
80
+ restart: (lease: UpdateRestartLeaseControl) => Promise<T>,
42
81
  ): Promise<{ readonly kind: "veto"; readonly notice: string } | { readonly kind: "ran"; readonly value: T }> {
43
- const lease = acquireOwnershipMutationLease(serviceStatePaths());
82
+ let lease = acquireOwnershipMutationLease(serviceStatePaths());
44
83
  const previous = process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
45
84
  process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = lease.token;
46
- try {
47
- const veto = updateRestartVeto(resolve);
48
- return veto ? { kind: "veto", notice: veto } : { kind: "ran", value: await restart() };
49
- } finally {
85
+ let heldNow = true;
86
+ const restoreEnv = () => {
50
87
  if (previous === undefined) delete process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
51
88
  else process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = previous;
89
+ };
90
+ const release = () => {
91
+ if (!heldNow) return;
92
+ heldNow = false;
93
+ restoreEnv();
52
94
  lease.release();
95
+ };
96
+ const reacquireForDirectStart = (): UpdateRestartStop | null => {
97
+ if (!heldNow) {
98
+ try {
99
+ lease = acquireOwnershipMutationLease(serviceStatePaths(), { waitMs: REACQUIRE_WAIT_MS });
100
+ } catch {
101
+ return {
102
+ failed: true,
103
+ notice: "Update installed, but another process kept the runtime ownership lease claimed, so no proxy was started. "
104
+ + "Run 'ocx service status', then 'ocx service repair' or 'ocx start'.",
105
+ };
106
+ }
107
+ heldNow = true;
108
+ process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = lease.token;
109
+ }
110
+ const veto = updateRestartVeto(resolve);
111
+ return veto ? { notice: veto, failed: false } : null;
112
+ };
113
+ try {
114
+ const veto = updateRestartVeto(resolve);
115
+ return veto
116
+ ? { kind: "veto", notice: veto }
117
+ : {
118
+ kind: "ran",
119
+ value: await restart({
120
+ releaseForServiceManager: release,
121
+ reacquireForDirectStart,
122
+ vetoAgain: () => updateRestartVeto(resolve),
123
+ }),
124
+ };
125
+ } finally {
126
+ release();
53
127
  }
54
128
  }
@@ -31,6 +31,8 @@ export function transactionalNpmUpdate(args: {
31
31
  targetVersion?: string;
32
32
  tag: string;
33
33
  runNpm: (args: string[]) => { status: number | null };
34
+ /** npm cache root the pre-flight resolved and checked; pinned with `--cache` for staging. */
35
+ cachePath?: string;
34
36
  log?: (line: string) => void;
35
37
  deps?: UpdateFsDeps;
36
38
  }): {
@@ -390,6 +390,7 @@ export function transactionalNpmUpdate({
390
390
  targetVersion,
391
391
  tag,
392
392
  runNpm,
393
+ cachePath,
393
394
  log = () => {},
394
395
  deps = {},
395
396
  }) {
@@ -432,8 +433,12 @@ export function transactionalNpmUpdate({
432
433
  // @oven/bun-* executable into bun/bin, so a successful npm exit without this narrow
433
434
  // approval leaves the staged tree intentionally incomplete. Allow only the package
434
435
  // whose executable the manifest verifies below; never broaden this to all scripts.
436
+ // --prefix <stage> also moves npm's globalconfig to <stage>/etc/npmrc, so a `cache=` from the
437
+ // operator's global npmrc would be dropped and npm would fall back to its default root. Pin
438
+ // the cache the pre-flight resolved and checked, so staging uses that exact root (#6288).
439
+ const cacheArgs = typeof cachePath === "string" && cachePath.length > 0 ? ["--cache", cachePath] : [];
435
440
  const install = runNpm([
436
- "install", "-g", "--prefix", stageRoot,
441
+ "install", "-g", "--prefix", stageRoot, ...cacheArgs,
437
442
  "--allow-scripts=bun", "--no-audit", "--no-fund", spec,
438
443
  ]);
439
444
  if (install.status !== 0) {
@@ -18,6 +18,7 @@ export const JEV_DECISION_GATES = [
18
18
  export type JevDecisionGate = (typeof JEV_DECISION_GATES)[number];
19
19
 
20
20
  const JEV_GATE_SET = new Set<string>(JEV_DECISION_GATES);
21
+ const JEV_STATS_BACKENDS = ["typesafe", "systemone", "model", "unknown"] as const;
21
22
  const JEV_EFFORTS = new Set<OcxComboDefaultEffort>([
22
23
  "low", "medium", "high", "xhigh", "max", "ultra",
23
24
  ]);
@@ -30,6 +31,7 @@ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/u;
30
31
  export interface PersistedJevDecisionV1 {
31
32
  version: 1;
32
33
  comboId: string;
34
+ backend?: "typesafe" | "systemone" | "model";
33
35
  selected: {
34
36
  provider: string;
35
37
  model: string;
@@ -103,6 +105,12 @@ export interface JevStatsResponse {
103
105
  averageChosenProbability: number | null;
104
106
  };
105
107
  gates: Array<{ gate: JevDecisionGate; decisions: number }>;
108
+ backends: Array<{
109
+ backend: "typesafe" | "systemone" | "model" | "unknown";
110
+ decisions: number;
111
+ applied: number;
112
+ averageLatencyMs: number | null;
113
+ }>;
106
114
  models: JevStatsModelRow[];
107
115
  snapshotWindowStart: number | null;
108
116
  snapshotWindowEnd: number | null;
@@ -174,9 +182,13 @@ export function normalizePersistedJevDecision(value: unknown): PersistedJevDecis
174
182
  const confidence = probability(value.confidence);
175
183
  const chosenProbability = probability(value.chosenProbability);
176
184
  const usage = normalizedDecisionUsage(value.usage);
185
+ const backend = value.backend === "typesafe" || value.backend === "systemone" || value.backend === "model"
186
+ ? value.backend
187
+ : undefined;
177
188
  return {
178
189
  version: 1,
179
190
  comboId,
191
+ ...(backend !== undefined ? { backend } : {}),
180
192
  selected: {
181
193
  provider,
182
194
  model,
@@ -269,6 +281,9 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
269
281
  private readonly models = new Map<string, MutableModelRow>();
270
282
  private overflowModel: MutableModelRow | null = null;
271
283
  private readonly gates = new Map<JevDecisionGate, number>();
284
+ private readonly backends = new Map<
285
+ JevStatsResponse["backends"][number]["backend"], JevStatsResponse["backends"][number]
286
+ >();
272
287
  private snapshotWindowStart: number | null = null;
273
288
  private snapshotWindowEnd: number | null = null;
274
289
  private decisions = 0;
@@ -300,7 +315,7 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
300
315
  }
301
316
 
302
317
  get estimatedBytes(): number {
303
- let bytes = 512;
318
+ let bytes = 512 + this.backends.size * 96;
304
319
  for (const row of this.models.values()) {
305
320
  bytes += 256 + (row.provider.length + row.model.length) * 2 + row.effortCounts.size * 32;
306
321
  }
@@ -340,6 +355,13 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
340
355
  const decision = normalizePersistedJevDecision(entry.jevDecision);
341
356
  if (!decision || (this.comboId !== null && decision.comboId !== this.comboId)) return;
342
357
 
358
+ const backend = decision.backend ?? "unknown";
359
+ const backendRow = this.backends.get(backend)
360
+ ?? { backend, decisions: 0, applied: 0, averageLatencyMs: null };
361
+ backendRow.averageLatencyMs = nextMean(backendRow.averageLatencyMs, backendRow.decisions, decision.latencyMs);
362
+ backendRow.decisions = saturatingAdd(backendRow.decisions, 1);
363
+ if (decision.gate === "apply") backendRow.applied = saturatingAdd(backendRow.applied, 1);
364
+ this.backends.set(backend, backendRow);
343
365
  this.averageLatencyMs = nextMean(this.averageLatencyMs, this.decisions, decision.latencyMs);
344
366
  this.decisions = saturatingAdd(this.decisions, 1);
345
367
  if (decision.gate === "apply") this.appliedDecisions = saturatingAdd(this.appliedDecisions, 1);
@@ -419,6 +441,7 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
419
441
  for (const [key, row] of this.models) cloned.models.set(key, cloneModelRow(row));
420
442
  cloned.overflowModel = this.overflowModel ? cloneModelRow(this.overflowModel) : null;
421
443
  for (const [gate, count] of this.gates) cloned.gates.set(gate, count);
444
+ for (const [backend, row] of this.backends) cloned.backends.set(backend, { ...row });
422
445
  cloned.snapshotWindowStart = this.snapshotWindowStart;
423
446
  cloned.snapshotWindowEnd = this.snapshotWindowEnd;
424
447
  cloned.decisions = this.decisions;
@@ -487,6 +510,10 @@ class StreamingJevStatsAccumulator implements JevStatsAccumulator {
487
510
  const count = this.gates.get(gate) ?? 0;
488
511
  return count > 0 ? [{ gate, decisions: count }] : [];
489
512
  }),
513
+ backends: JEV_STATS_BACKENDS.flatMap(backend => {
514
+ const row = this.backends.get(backend);
515
+ return row && row.decisions > 0 ? [{ ...row }] : [];
516
+ }),
490
517
  models: rows,
491
518
  snapshotWindowStart: this.snapshotWindowStart,
492
519
  snapshotWindowEnd: this.snapshotWindowEnd,
package/src/usage/log.ts CHANGED
@@ -506,7 +506,7 @@ const KNOWN_AFFINITY_MOVES = new Set<NonNullable<PersistedUsageEntry["affinity"]
506
506
  ]);
507
507
  const KNOWN_AFFINITY_REASONS = new Set<NonNullable<PersistedUsageEntry["affinityReason"]>>([
508
508
  "healthy", "quota_headroom", "quota_refusal", "transient", "transient_hold_expired",
509
- "unusable", "paused", "plan_excluded", "cooldown", "quota_avoided", "generation",
509
+ "unusable", "paused", "plan_excluded", "credits_off", "cooldown", "quota_avoided", "generation",
510
510
  "expired", "model_lane",
511
511
  ]);
512
512
  const KNOWN_CONVERSATION_STATE_SCRUBS = new Set<NonNullable<PersistedUsageEntry["conversationStateScrub"]>>([
@@ -1,10 +1,11 @@
1
- import type { OcxProviderConfig } from "../types";
1
+ import type { OcxConfig, OcxProviderConfig } from "../types";
2
2
  import { sidecarThinkingOff } from "../adapters/anthropic-model-contract";
3
3
  import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "../adapters/client-fingerprint";
4
4
  import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
5
5
  import { sidecarEnter } from "../lib/sidecar-tracker";
6
6
  import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-retry";
7
- import { getValidAccessToken, publicOAuthAuthenticationErrorMessage } from "../oauth";
7
+ import { publicOAuthAuthenticationErrorMessage } from "../oauth";
8
+ import { getAnthropicSidecarAccessToken } from "../oauth/anthropic-routing";
8
9
  import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION } from "../oauth/anthropic";
9
10
  import type { DescribeOutcome, VisionSettings } from "./describe";
10
11
 
@@ -152,13 +153,14 @@ export async function describeImageAnthropic(
152
153
  provider: OcxProviderConfig,
153
154
  settings: VisionSettings,
154
155
  abortSignal?: AbortSignal,
156
+ config?: OcxConfig,
155
157
  ): Promise<DescribeOutcome> {
156
158
  const image = buildImageBlock(imageUrl);
157
159
  if (!image.block) return { text: "", error: image.error ?? "invalid image" };
158
160
 
159
161
  let token: string;
160
162
  try {
161
- token = await getValidAccessToken(providerName);
163
+ token = await getAnthropicSidecarAccessToken(providerName, settings.model, config);
162
164
  } catch (error) {
163
165
  return { text: "", error: `anthropic vision sidecar auth failed: ${publicOAuthAuthenticationErrorMessage(error)}` };
164
166
  }
@@ -256,6 +256,7 @@ async function executeDescription(
256
256
  sidecar.provider,
257
257
  plan.settings,
258
258
  abortSignal,
259
+ sidecar.config,
259
260
  );
260
261
  }
261
262
  if (!plan.forwardSidecar) return { text: "", error: "OpenAI vision sidecar is unavailable" };
@@ -43,6 +43,7 @@ export function resolveVisionTimeoutMs(value: unknown): number {
43
43
  export interface AnthropicVisionProvider {
44
44
  providerName: string;
45
45
  provider: OcxProviderConfig;
46
+ config: OcxConfig;
46
47
  }
47
48
 
48
49
  /**
@@ -52,7 +53,7 @@ export interface AnthropicVisionProvider {
52
53
  export function findAnthropicVisionProvider(config: OcxConfig): AnthropicVisionProvider | undefined {
53
54
  const auth = resolveSidecarAuth(config);
54
55
  if (!auth.isAnthropicAuth || !auth.anthropicProviderName || !auth.anthropicProvider) return undefined;
55
- return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider };
56
+ return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config };
56
57
  }
57
58
 
58
59
  export function resolveVisionBackend(