@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
package/bin/ocx.mjs CHANGED
@@ -9,6 +9,9 @@
9
9
  * src/cli/index.ts — only the published npm/pnpm `bin` routes through here.)
10
10
  */
11
11
  import { spawn, spawnSync } from "node:child_process";
12
+ import { createSupervisionLatch, inspectDesktopSupervision } from "../src/service/desktop-supervision.mjs";
13
+ import { planDesktopCliHandoff, runDesktopCliHandoff } from "../src/lib/desktop-cli-handoff.mjs";
14
+ import { desktopCliRecordPath } from "../src/lib/desktop-cli-record.mjs";
12
15
  import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../src/update/stop-contract.mjs";
13
16
  import { probeProxyLiveness } from "../src/update/proxy-liveness-probe.mjs";
14
17
  import { decidePostStopUpdate } from "../src/update/stop-decision.mjs";
@@ -34,6 +37,7 @@ import { homedir } from "node:os";
34
37
  import { dirname, join, resolve } from "node:path";
35
38
  import { fileURLToPath } from "node:url";
36
39
  import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";
40
+ import { findDesktopCli, findPathBun } from "../src/lib/bun-path-runtime.mjs";
37
41
  import { npmInvocation } from "../src/update/npm-invocation.mjs";
38
42
  import { pnpmInvocationForPath, resolvePnpmCommands } from "../src/update/pnpm-invocation.mjs";
39
43
  import { detectInstallOwnershipFromPath } from "../src/update/install-detection.mjs";
@@ -95,6 +99,15 @@ function currentPackageVersion() {
95
99
  }
96
100
  }
97
101
 
102
+ function pinnedBunVersion() {
103
+ try {
104
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
105
+ return typeof pkg.dependencies?.bun === "string" ? pkg.dependencies.bun : "";
106
+ } catch {
107
+ return "";
108
+ }
109
+ }
110
+
98
111
  function updateTag(currentVersion) {
99
112
  // Allowlist the tag: the value is argv-controlled and (on Windows) flows into a
100
113
  // shell-joined spawnSync — never forward arbitrary strings.
@@ -323,11 +336,13 @@ function runPackageManagerSelfUpdate(manager) {
323
336
  : JSON.stringify(observation.ownership
324
337
  ? ["owned", observation.ownership.owner, observation.ownership.installId, observation.ownership.consentGeneration]
325
338
  : ["none"]);
339
+ const supervisionLatch = createSupervisionLatch();
340
+ const observeSupervision = () => supervisionLatch.observe(inspectDesktopSupervision());
326
341
  const initialOwnership = readOwnership();
327
- let runtimePlan = planUpdateRuntimeHandling({ ...initialOwnership, serviceInstalled: serviceWasInstalled });
342
+ let runtimePlan = planUpdateRuntimeHandling({ ...initialOwnership, serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
328
343
  if (runtimePlan.notice) console.log(runtimePlan.notice);
329
344
  if (!runtimePlan.mayReplacePackage) {
330
- console.error("opencodex: update stopped before tray handoff, runtime stop, or package replacement because runtime ownership is unknown.");
345
+ console.error("opencodex: update stopped before tray handoff, runtime stop, or package replacement because runtime authority does not permit it.");
331
346
  process.exit(1);
332
347
  }
333
348
  const trayBeforeUpdate = planWindowsTrayUpdate(
@@ -456,6 +471,14 @@ function runPackageManagerSelfUpdate(manager) {
456
471
  : unprivilegedOwnershipMutationEnvironment(process.env);
457
472
 
458
473
  function startProxyDirectly() {
474
+ const supervision = observeSupervision();
475
+ const plan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: false, supervision });
476
+ if (!plan.mayStopRuntime) {
477
+ console.warn(supervision
478
+ ? "OpenCodex Desktop supervises the proxy; no CLI runtime was restored."
479
+ : plan.notice);
480
+ return false;
481
+ }
459
482
  if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) {
460
483
  console.error("opencodex: cannot restart the proxy because the launcher is missing; reinstall opencodex manually.");
461
484
  return false;
@@ -488,6 +511,15 @@ function runPackageManagerSelfUpdate(manager) {
488
511
  }
489
512
 
490
513
  function refreshBackgroundServiceOrStartDirect() {
514
+ const mayRefresh = () => {
515
+ const supervision = observeSupervision();
516
+ const plan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: true, supervision });
517
+ if (!plan.mayRestoreService) console.warn(supervision
518
+ ? "OpenCodex Desktop supervises the proxy; CLI recovery was skipped. Use the app's updater (tray → Check for Updates)."
519
+ : plan.notice);
520
+ return plan.mayRestoreService;
521
+ };
522
+ if (!mayRefresh()) return;
491
523
  const prevBake = process.env.OCX_BAKE_PORT;
492
524
  process.env.OCX_BAKE_PORT = String(bakePort);
493
525
  try {
@@ -503,6 +535,7 @@ function runPackageManagerSelfUpdate(manager) {
503
535
  // failure would resurrect the elevation prompt this change exists to avoid, and
504
536
  // could re-register a service the user just uninstalled.
505
537
  if (svc.status !== 0 && readServiceInstalledFromStatus(postUpdateLauncher) === false) {
538
+ if (!mayRefresh()) return;
506
539
  console.log("No registered service found — installing it instead.");
507
540
  svc = spawnSync(process.execPath, serviceInstallArgs(), {
508
541
  stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
@@ -535,9 +568,12 @@ function runPackageManagerSelfUpdate(manager) {
535
568
  // Re-read rather than reuse the plan from before the package install: the app can
536
569
  // claim the runtime during an update that takes minutes, and the refusal that repair
537
570
  // just returned is indistinguishable from any other failure at this layer.
538
- const nowOwned = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: true });
571
+ const supervision = observeSupervision();
572
+ const nowOwned = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: true, supervision });
539
573
  if (!nowOwned.mayStopRuntime) {
540
- console.warn(nowOwned.notice ?? "opencodex: the background runtime is owned elsewhere; not starting a second proxy.");
574
+ console.warn(supervision
575
+ ? "OpenCodex Desktop supervises the proxy; CLI recovery was skipped. Use the app's updater (tray → Check for Updates)."
576
+ : nowOwned.notice ?? "opencodex: the background runtime is owned elsewhere; not starting a second proxy.");
541
577
  return;
542
578
  }
543
579
  // Repair normally avoids elevation for a healthy registration, but a stale Windows
@@ -574,7 +610,7 @@ function runPackageManagerSelfUpdate(manager) {
574
610
  // Stop authority is decided under the same lease the child joins. A takeover between the
575
611
  // earlier preflight and this boundary therefore blocks stop before it is sent.
576
612
  const lockedOwnership = readOwnership();
577
- const lockedPlan = planUpdateRuntimeHandling({ ...lockedOwnership, serviceInstalled: serviceWasInstalled });
613
+ const lockedPlan = planUpdateRuntimeHandling({ ...lockedOwnership, serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
578
614
  if (lockedOwnership.subjectToken !== initialOwnership.subjectToken || !lockedPlan.mayReplacePackage) {
579
615
  releaseUpdateLease();
580
616
  console.error(lockedPlan.notice
@@ -616,6 +652,7 @@ function runPackageManagerSelfUpdate(manager) {
616
652
  liveness,
617
653
  plan: planStoppedRuntimeRecovery({
618
654
  stopAttempted,
655
+ supervision: observeSupervision(),
619
656
  ...recoveryOwnership,
620
657
  sameOwner: ownershipIdentity(recoveryOwnership) === stoppedOwnershipIdentity,
621
658
  liveness,
@@ -633,7 +670,9 @@ function runPackageManagerSelfUpdate(manager) {
633
670
  releaseUpdateLease();
634
671
  ({ liveness: recoveryLiveness, plan: recovery } = planRecovery());
635
672
  }
636
- if (recovery.reason === "ownership-unknown") {
673
+ if (recovery.reason === "desktop-supervised") {
674
+ console.log("OpenCodex Desktop supervises the proxy; no CLI runtime was restored.");
675
+ } else if (recovery.reason === "ownership-unknown") {
637
676
  console.error(`opencodex: ${reason}; runtime ownership is unknown, so automatic recovery was refused. Run 'ocx status --json' and repair the service-state record before retrying.`);
638
677
  } else if (recovery.reason === "ownership-transferred") {
639
678
  console.log("opencodex: runtime ownership moved to another installation; the stopped CLI runtime was not revived.");
@@ -666,6 +705,14 @@ function runPackageManagerSelfUpdate(manager) {
666
705
  process.exit(1);
667
706
  }
668
707
  if (stopNeeded) {
708
+ const preStopPlan = planUpdateRuntimeHandling({
709
+ ...readOwnership(), serviceInstalled: serviceWasInstalled, supervision: observeSupervision(),
710
+ });
711
+ if (!preStopPlan.mayStopRuntime) {
712
+ console.error(preStopPlan.notice);
713
+ releaseUpdateLease();
714
+ process.exit(1);
715
+ }
669
716
  stopAttempted = true;
670
717
  console.log("⏹ Stopping the running proxy before updating...");
671
718
  const stopRes = spawnSync(process.execPath, [launcher, "stop"], {
@@ -722,7 +769,7 @@ function runPackageManagerSelfUpdate(manager) {
722
769
  }
723
770
 
724
771
  const replacementOwnership = readOwnership();
725
- const replacementPlan = planUpdateRuntimeHandling({ ...replacementOwnership, serviceInstalled: serviceWasInstalled });
772
+ const replacementPlan = planUpdateRuntimeHandling({ ...replacementOwnership, serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
726
773
  const replacementLiveness = currentPackageRuntimeLiveness();
727
774
  if (replacementOwnership.subjectToken !== initialOwnership.subjectToken
728
775
  || !replacementPlan.mayReplacePackage
@@ -829,9 +876,10 @@ function runPackageManagerSelfUpdate(manager) {
829
876
  // path and keeps token restoration coupled to the lease itself.
830
877
  releaseUpdateLease();
831
878
  }
832
- const postInstallPlan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: serviceWasInstalled });
879
+ const postInstallPlan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
833
880
  if (res.status === 0) {
834
881
  console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`);
882
+ if (!postInstallPlan.mayStopRuntime) console.warn("Runtime authority does not permit CLI recovery; no service was refreshed or proxy started.");
835
883
  repairCodexShimIfNeeded(postUpdateLauncher);
836
884
  if (trayBeforeUpdate.refreshAfterReplacement) {
837
885
  const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayBeforeUpdate.installArgs], {
@@ -884,6 +932,8 @@ const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
884
932
  // imported; tests/cli/ocx-launcher-source.test.ts pins the two together.
885
933
  const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
886
934
  const BUN_RUNTIME_PATH_ENV = "OCX_BUN_RUNTIME_PATH";
935
+ /** Total budget for validating a PATH Bun fallback (both probes together). */
936
+ const PATH_BUN_PROBE_BUDGET_MS = 5_000;
887
937
 
888
938
  function findBunBinary(bunDir) {
889
939
  // The bundled `bun` package ships the binary as bin/bun.exe on every platform;
@@ -899,6 +949,7 @@ function fail(msg) {
899
949
  const reinstall = installMethod === "pnpm"
900
950
  ? "pnpm add -g --allow-build=bun @bitkyc08/opencodex"
901
951
  : "npm install -g --allow-scripts=bun @bitkyc08/opencodex";
952
+ const desktopCli = findDesktopCli();
902
953
  console.error(
903
954
  `opencodex: ${msg}\n` +
904
955
  "The bundled Bun runtime could not be prepared. This usually means the\n" +
@@ -906,7 +957,8 @@ function fail(msg) {
906
957
  "or pnpm did not approve bun's build) or optional dependencies. Reinstall with:\n" +
907
958
  ` ${reinstall}\n` +
908
959
  "(use sudo if the original install used sudo; without --ignore-scripts\n" +
909
- "and without --omit=optional / optional=false)"
960
+ "and without --omit=optional / optional=false)" +
961
+ (desktopCli ? `\nAn installed Desktop CLI is available: "${desktopCli}"` : "")
910
962
  );
911
963
  process.exit(1);
912
964
  }
@@ -923,25 +975,32 @@ function resolveBun({ allowInstall = true } = {}) {
923
975
  );
924
976
  }
925
977
 
926
- let bunDir;
978
+ let bunDir = null;
927
979
  try {
928
980
  bunDir = bunBinDir();
929
- } catch {
930
- fail("the `bun` dependency is not installed.");
931
- }
981
+ } catch { /* Missing dependency can still fall back to a validated PATH Bun. */ }
932
982
 
933
- let bin = findBunBinary(bunDir);
983
+ let bin = bunDir ? findBunBinary(bunDir) : null;
934
984
  if (bin) return { path: bin, source: "bundled" };
935
985
 
936
986
  // Lazy fallback: --ignore-scripts (or a failed postinstall) leaves the
937
987
  // ~450-byte placeholder stub. Run the bun package's own installer once.
938
- const installJs = join(bunDir, "install.js");
939
- if (allowInstall && existsSync(installJs)) {
988
+ const installJs = bunDir ? join(bunDir, "install.js") : null;
989
+ if (allowInstall && installJs && existsSync(installJs)) {
940
990
  const r = spawnSync(process.execPath, [installJs], { stdio: "inherit" });
941
991
  if (r.status === 0) bin = findBunBinary(bunDir);
942
992
  }
943
- if (!bin) fail("Bun binary missing after install attempt.");
944
- return { path: bin, source: "bundled" };
993
+ if (bin) return { path: bin, source: "bundled" };
994
+
995
+ // Reached only when the bundled runtime is unusable. The two probes start the candidate Bun
996
+ // cold, and on Windows a first run of a copied bun.exe is often held by an on-access scan for
997
+ // well over a second, so the total budget is generous rather than interactive-tight.
998
+ const pathBun = findPathBun({ pinnedVersion: pinnedBunVersion(), deadlineMs: PATH_BUN_PROBE_BUDGET_MS });
999
+ if (pathBun) {
1000
+ console.error(`opencodex: using PATH Bun ${pathBun.version}.`);
1001
+ return { path: pathBun.path, source: "process" };
1002
+ }
1003
+ fail(bunDir ? "Bun binary missing after install attempt." : "the `bun` dependency is not installed.");
945
1004
  }
946
1005
 
947
1006
  // `ocx update --help` prints usage and exits WITHOUT side effects. The Node launcher
@@ -961,6 +1020,36 @@ if (codexCliUpdateInspection && typeof process.versions.bun === "string") {
961
1020
  process.exit(1);
962
1021
  }
963
1022
 
1023
+ const { launchProof, launchContext, inheritedEnv } = createNodeLaunchContext();
1024
+ const desktopPlan = planDesktopCliHandoff({ selfPaths: [fileURLToPath(import.meta.url), process.execPath] });
1025
+ if (desktopPlan.kind === "error") {
1026
+ if (desktopPlan.issue.startsWith("record-")) {
1027
+ console.error(`opencodex: OpenCodex Desktop's terminal-command record at ${desktopCliRecordPath()} could not be used (${desktopPlan.issue}). Open OpenCodex Desktop to repair the terminal command, or run with OCX_NO_DESKTOP_HANDOFF=1.`);
1028
+ } else {
1029
+ console.error(`opencodex: Desktop CLI handoff refused (${desktopPlan.issue}). Repair the terminal command in OpenCodex Desktop, or run with OCX_NO_DESKTOP_HANDOFF=1.`);
1030
+ }
1031
+ process.exit(1);
1032
+ }
1033
+ if (desktopPlan.kind === "handoff") {
1034
+ const result = await runDesktopCliHandoff(desktopPlan, {
1035
+ argv: process.argv.slice(2), env: inheritedEnv, proof: launchProof, context: launchContext,
1036
+ });
1037
+ if (result.kind === "error") {
1038
+ console.error(`opencodex: Desktop CLI handoff failed (${result.issue}). Repair the terminal command in OpenCodex Desktop, or run with OCX_NO_DESKTOP_HANDOFF=1.`);
1039
+ process.exit(127);
1040
+ }
1041
+ if (result.kind === "exit") {
1042
+ if (result.signal) {
1043
+ process.kill(process.pid, result.signal);
1044
+ } else {
1045
+ process.exit(result.code);
1046
+ }
1047
+ // Signal delivery is asynchronous; never resume package repair or Bun lookup.
1048
+ await new Promise(() => {});
1049
+ }
1050
+ // Only confirmed target ENOENT reaches the ordinary package path below.
1051
+ }
1052
+
964
1053
  if (process.argv[2] === "update" && installMethod === "mise") {
965
1054
  if (installOwnership.owner) {
966
1055
  console.error(
@@ -1015,64 +1104,68 @@ const bun = bunRuntime.path;
1015
1104
  // billing and prevents it from redirecting the subscriber's OAuth bearer.
1016
1105
  // Disabling Bun's dotenv wholesale with --no-env-file is NOT an option: config
1017
1106
  // interpolation and provider settings legitimately read the project environment.
1018
- const preBunAnthropicSlots = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"]
1019
- .filter(name => typeof process.env[name] === "string" && process.env[name] !== "");
1020
- // A configured CODEX_CLI_PATH may legitimately be cwd-relative (`./tools/codex`), which the
1021
- // ordinary runtime resolver accepts. Inspection only trusts absolute local paths, so capture
1022
- // the absolute form here, in the launcher, while the original cwd is still authoritative;
1023
- // resolving it later would silently reinterpret it against a different working directory.
1024
- //
1025
- // A bare command with no separator (`codex`) is NOT a relative path: the runtime resolver
1026
- // deliberately hands those to executable lookup along PATH. Rewriting it to `<cwd>/codex`
1027
- // would make the inspector treat it as an explicit path and stop searching PATH entirely.
1028
- const configuredCodexCliPath = typeof process.env.CODEX_CLI_PATH === "string" && process.env.CODEX_CLI_PATH !== ""
1029
- ? process.env.CODEX_CLI_PATH
1030
- : null;
1031
- const preBunCodexCliPath = configuredCodexCliPath !== null
1032
- && (configuredCodexCliPath.includes("/") || configuredCodexCliPath.includes("\\") || /^[A-Za-z]:/.test(configuredCodexCliPath))
1033
- ? resolve(configuredCodexCliPath)
1034
- : configuredCodexCliPath;
1035
- const preBunPath = typeof process.env.PATH === "string" ? process.env.PATH : null;
1036
- const preBunPathExt = typeof process.env.PATHEXT === "string" ? process.env.PATHEXT : null;
1037
- const preBunCodexCliManagerRoots = Object.fromEntries(
1038
- CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS.flatMap(name => {
1039
- const value = process.env[name];
1040
- return typeof value === "string" && value !== "" ? [[name, value]] : [];
1041
- }),
1042
- );
1043
- const launchProof = randomBytes(32).toString("base64url");
1044
- const launchContext = JSON.stringify({
1045
- version: 1,
1046
- proof: launchProof,
1047
- anthropicEnvSlots: preBunAnthropicSlots,
1048
- codexCliInspectionEnv: codexCliUpdateInspection ? {
1049
- codexCliPath: preBunCodexCliPath,
1050
- path: preBunPath,
1051
- pathExt: preBunPathExt,
1052
- managerRoots: preBunCodexCliManagerRoots,
1053
- configDir: configDir(),
1054
- } : null,
1055
- });
1056
- // The inspection snapshot above already carries PATH, PATHEXT, and the manager-root slots as
1057
- // proof-bound values, and `inspectCodexCliInstall` reads them from that snapshot rather than
1058
- // from the live environment. Inheriting them again would spend the 32,767-character Windows
1059
- // environment block twice, so a large-but-valid shell environment could stop the Bun child
1060
- // from spawning and fail the command before it reports anything. Drop the duplicates for the
1061
- // one-shot inspection launch only; every other launch inherits the environment unchanged.
1062
- // Windows environment names are case-insensitive, but this spread produces an ordinary
1063
- // case-sensitive object, and a real Windows environment commonly spells the variable `Path`.
1064
- // Deleting only the canonical upper-case spelling would silently leave that copy behind and
1065
- // reintroduce the duplication this block exists to prevent, so match on the lowercase form.
1066
- const inheritedEnv = { ...process.env };
1067
- if (codexCliUpdateInspection) {
1068
- const snapshotted = new Set(
1069
- ["PATH", "PATHEXT", ...CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS].map(name => name.toLowerCase()),
1107
+ function createNodeLaunchContext() {
1108
+ const preBunAnthropicSlots = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"]
1109
+ .filter(name => typeof process.env[name] === "string" && process.env[name] !== "");
1110
+ // A configured CODEX_CLI_PATH may legitimately be cwd-relative (`./tools/codex`), which the
1111
+ // ordinary runtime resolver accepts. Inspection only trusts absolute local paths, so capture
1112
+ // the absolute form here, in the launcher, while the original cwd is still authoritative;
1113
+ // resolving it later would silently reinterpret it against a different working directory.
1114
+ //
1115
+ // A bare command with no separator (`codex`) is NOT a relative path: the runtime resolver
1116
+ // deliberately hands those to executable lookup along PATH. Rewriting it to `<cwd>/codex`
1117
+ // would make the inspector treat it as an explicit path and stop searching PATH entirely.
1118
+ const configuredCodexCliPath = typeof process.env.CODEX_CLI_PATH === "string" && process.env.CODEX_CLI_PATH !== ""
1119
+ ? process.env.CODEX_CLI_PATH
1120
+ : null;
1121
+ const preBunCodexCliPath = configuredCodexCliPath !== null
1122
+ && (configuredCodexCliPath.includes("/") || configuredCodexCliPath.includes("\\") || /^[A-Za-z]:/.test(configuredCodexCliPath))
1123
+ ? resolve(configuredCodexCliPath)
1124
+ : configuredCodexCliPath;
1125
+ const preBunPath = typeof process.env.PATH === "string" ? process.env.PATH : null;
1126
+ const preBunPathExt = typeof process.env.PATHEXT === "string" ? process.env.PATHEXT : null;
1127
+ const preBunCodexCliManagerRoots = Object.fromEntries(
1128
+ CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS.flatMap(name => {
1129
+ const value = process.env[name];
1130
+ return typeof value === "string" && value !== "" ? [[name, value]] : [];
1131
+ }),
1070
1132
  );
1071
- for (const name of Object.keys(inheritedEnv)) {
1072
- if (snapshotted.has(name.toLowerCase())) delete inheritedEnv[name];
1133
+ const launchProof = randomBytes(32).toString("base64url");
1134
+ const launchContext = JSON.stringify({
1135
+ version: 1,
1136
+ proof: launchProof,
1137
+ anthropicEnvSlots: preBunAnthropicSlots,
1138
+ codexCliInspectionEnv: codexCliUpdateInspection ? {
1139
+ codexCliPath: preBunCodexCliPath,
1140
+ path: preBunPath,
1141
+ pathExt: preBunPathExt,
1142
+ managerRoots: preBunCodexCliManagerRoots,
1143
+ configDir: configDir(),
1144
+ } : null,
1145
+ });
1146
+ // The inspection snapshot above already carries PATH, PATHEXT, and the manager-root slots as
1147
+ // proof-bound values, and `inspectCodexCliInstall` reads them from that snapshot rather than
1148
+ // from the live environment. Inheriting them again would spend the 32,767-character Windows
1149
+ // environment block twice, so a large-but-valid shell environment could stop the Bun child
1150
+ // from spawning and fail the command before it reports anything. Drop the duplicates for the
1151
+ // one-shot inspection launch only; every other launch inherits the environment unchanged.
1152
+ // Windows environment names are case-insensitive, but this spread produces an ordinary
1153
+ // case-sensitive object, and a real Windows environment commonly spells the variable `Path`.
1154
+ // Deleting only the canonical upper-case spelling would silently leave that copy behind and
1155
+ // reintroduce the duplication this block exists to prevent, so match on the lowercase form.
1156
+ const inheritedEnv = { ...process.env };
1157
+ if (codexCliUpdateInspection) {
1158
+ const snapshotted = new Set(
1159
+ ["PATH", "PATHEXT", ...CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS].map(name => name.toLowerCase()),
1160
+ );
1161
+ for (const name of Object.keys(inheritedEnv)) {
1162
+ if (snapshotted.has(name.toLowerCase())) delete inheritedEnv[name];
1163
+ }
1073
1164
  }
1165
+ return { launchProof, launchContext, inheritedEnv };
1074
1166
  }
1075
- const child = spawn(bun, [cliPath, `${NODE_LAUNCH_PROOF_PREFIX}${launchProof}`, ...process.argv.slice(2)], {
1167
+
1168
+ const child = spawn(bun, [cliPath, `${NODE_LAUNCH_PROOF_PREFIX}${launchProof}`, ...process.argv.slice(2).filter(arg => !arg.startsWith(NODE_LAUNCH_PROOF_PREFIX))], {
1076
1169
  stdio: "inherit",
1077
1170
  // A headless Windows parent (Task Scheduler, dashboard restart, shortcut) has no
1078
1171
  // console to inherit. Without this flag Windows allocates a visible console for