@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922

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 (249) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +5 -1
  13. package/src/adapters/anthropic.ts +16 -0
  14. package/src/adapters/coding-agent/protocol.ts +36 -6
  15. package/src/adapters/coding-agent/turn.ts +10 -2
  16. package/src/adapters/command-code.ts +2 -1
  17. package/src/adapters/cursor/catalog.ts +51 -7
  18. package/src/adapters/cursor/protobuf-request.ts +6 -3
  19. package/src/adapters/cursor/request-builder.ts +13 -3
  20. package/src/adapters/cursor.ts +11 -2
  21. package/src/adapters/declaration-carrier.ts +45 -0
  22. package/src/adapters/devin.ts +75 -23
  23. package/src/adapters/google-antigravity-wire.ts +5 -2
  24. package/src/adapters/google-errors.ts +7 -1
  25. package/src/adapters/google.ts +29 -5
  26. package/src/adapters/image.ts +4 -1
  27. package/src/adapters/input-media-guard.ts +21 -9
  28. package/src/adapters/kiro/usage.ts +3 -2
  29. package/src/adapters/kiro-tool-fallback.ts +1 -1
  30. package/src/adapters/ollama-native.ts +6 -0
  31. package/src/adapters/openai-chat/developer-role.ts +61 -0
  32. package/src/adapters/openai-chat/messages.ts +46 -27
  33. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  34. package/src/adapters/openai-chat/passthrough.ts +33 -9
  35. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  36. package/src/adapters/openai-chat.ts +18 -57
  37. package/src/adapters/openai-responses/passthrough.ts +2 -0
  38. package/src/adapters/registry.ts +3 -2
  39. package/src/adapters/run-turn-queue.ts +178 -29
  40. package/src/adapters/xai-web-search.ts +16 -1
  41. package/src/bridge/errors.ts +8 -2
  42. package/src/bridge/response-json.ts +9 -1
  43. package/src/bridge/sse.ts +10 -0
  44. package/src/chat/inbound.ts +141 -5
  45. package/src/claude/desktop-3p.ts +7 -1
  46. package/src/claude/desktop-first-party.ts +183 -0
  47. package/src/claude/desktop-gateway-state.ts +41 -0
  48. package/src/claude/inbound-content-options.ts +6 -0
  49. package/src/claude/inbound.ts +32 -6
  50. package/src/claude/intercept/connect-proxy.ts +179 -0
  51. package/src/claude/intercept/listener.ts +122 -0
  52. package/src/claude/intercept/local-ca.ts +298 -0
  53. package/src/claude/intercept/runtime.ts +98 -0
  54. package/src/claude/intercept/settings.ts +189 -0
  55. package/src/cli/access.ts +87 -0
  56. package/src/cli/account-auth.ts +19 -0
  57. package/src/cli/capabilities.ts +31 -0
  58. package/src/cli/claude-desktop.ts +206 -16
  59. package/src/cli/codex-shim-autorestore.ts +3 -0
  60. package/src/cli/companion.ts +56 -0
  61. package/src/cli/dispatch.ts +43 -4
  62. package/src/cli/ensure-desired-integrations.ts +43 -5
  63. package/src/cli/help.ts +7 -9
  64. package/src/cli/index.ts +200 -61
  65. package/src/cli/init.ts +8 -0
  66. package/src/cli/integrations.ts +7 -1
  67. package/src/cli/registry.ts +41 -2
  68. package/src/cli/resolve.ts +230 -0
  69. package/src/cli/root.ts +24 -1
  70. package/src/cli/start-ownership-publication.ts +56 -0
  71. package/src/cli/status-probes.ts +2 -18
  72. package/src/cli/status.ts +62 -0
  73. package/src/cli/stop-report.ts +143 -0
  74. package/src/cli/uninstall-plan.ts +9 -0
  75. package/src/client/machine-listener.ts +2 -5
  76. package/src/clients/aside-profiles.ts +4 -0
  77. package/src/clients/config-export/zcode-store.ts +157 -0
  78. package/src/clients/config-export.ts +36 -0
  79. package/src/codex/app-server-processes.ts +72 -40
  80. package/src/codex/auth-api/login-flow.ts +6 -1
  81. package/src/codex/autostart-health.ts +28 -0
  82. package/src/codex/catalog/build-entries.ts +2 -2
  83. package/src/codex/catalog/effort.ts +3 -3
  84. package/src/codex/catalog/provider-models.ts +24 -15
  85. package/src/codex/catalog/retained-sync.ts +2 -2
  86. package/src/codex/convergence.ts +2 -2
  87. package/src/codex/history-provider.ts +12 -1
  88. package/src/codex/inject/config-toml.ts +41 -6
  89. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  90. package/src/codex/inject.ts +18 -15
  91. package/src/codex/injected-marker.ts +18 -0
  92. package/src/codex/main-account.ts +6 -0
  93. package/src/codex/model-cache.ts +52 -6
  94. package/src/codex/model-entitlement-admission.ts +59 -0
  95. package/src/codex/model-entitlements.ts +87 -44
  96. package/src/codex/native-main-admission.ts +83 -0
  97. package/src/codex/routing/health-store.ts +39 -0
  98. package/src/codex/routing/selection.ts +37 -1
  99. package/src/codex/routing.ts +5 -41
  100. package/src/codex/shim-templates.ts +29 -3
  101. package/src/companion/settings.ts +132 -0
  102. package/src/config/atomic-write.ts +117 -5
  103. package/src/config/load-degrade.ts +34 -7
  104. package/src/config/process-state.ts +1 -1
  105. package/src/config/schema/config-schema.ts +27 -1
  106. package/src/config/schema/leaf-validators.ts +47 -0
  107. package/src/config.ts +1 -1
  108. package/src/generated/compatibility-version.json +418 -174
  109. package/src/integrations/config-io.ts +44 -10
  110. package/src/integrations/merge.ts +120 -13
  111. package/src/integrations/mutation-plan.ts +124 -18
  112. package/src/integrations/registry.ts +38 -0
  113. package/src/integrations/state.ts +78 -45
  114. package/src/integrations/target.ts +208 -0
  115. package/src/integrations/writer.ts +49 -11
  116. package/src/lab/conformance/fixture-provider.ts +5 -0
  117. package/src/lib/browser-launch-notice.ts +59 -0
  118. package/src/lib/bun-runtime.ts +6 -2
  119. package/src/lib/debug.ts +40 -0
  120. package/src/lib/open-url.ts +51 -7
  121. package/src/lib/package-tree-integrity.ts +2 -1
  122. package/src/lib/package-version.ts +8 -0
  123. package/src/lib/provider-egress.ts +310 -0
  124. package/src/lib/provider-outbound.ts +59 -14
  125. package/src/lib/proxy-env.ts +82 -7
  126. package/src/lib/request-execution-budget.ts +72 -0
  127. package/src/lib/request-failure-attribution.ts +183 -0
  128. package/src/lib/request-failure-model.ts +236 -0
  129. package/src/lib/request-resend-gate.ts +138 -0
  130. package/src/lib/standalone.ts +16 -0
  131. package/src/lib/upstream-retry.ts +167 -16
  132. package/src/lib/winsw.ts +2 -2
  133. package/src/oauth/index.ts +24 -1
  134. package/src/oauth/login-cli.ts +80 -29
  135. package/src/providers/api-key-resolve.ts +133 -0
  136. package/src/providers/api-key-selection.ts +5 -1
  137. package/src/providers/key-failover.ts +31 -1
  138. package/src/providers/key-store.ts +34 -110
  139. package/src/providers/model-rename-fields.ts +147 -0
  140. package/src/providers/model-rename-migration.ts +124 -37
  141. package/src/providers/quota/vendor-probes-key.ts +37 -22
  142. package/src/providers/reasoning-metadata.ts +43 -18
  143. package/src/providers/registry/entries-core.ts +9 -4
  144. package/src/providers/registry/entries-extended.ts +29 -4
  145. package/src/providers/registry/model-seeds.ts +47 -10
  146. package/src/providers/xai-transport.ts +12 -1
  147. package/src/reasoning-effort.ts +8 -0
  148. package/src/responses/function-call-compat.ts +38 -1
  149. package/src/responses/inline-document.ts +65 -0
  150. package/src/responses/input-media.ts +42 -8
  151. package/src/responses/muse-tool-name-alias.ts +19 -0
  152. package/src/responses/parser-content.ts +8 -2
  153. package/src/responses/parser-tools.ts +3 -0
  154. package/src/responses/parser.ts +3 -1
  155. package/src/responses/schema.ts +3 -0
  156. package/src/router.ts +17 -2
  157. package/src/server/admission-model-scope.ts +219 -0
  158. package/src/server/audio-live.ts +9 -3
  159. package/src/server/audio-upstream.ts +18 -0
  160. package/src/server/auth-cors.ts +26 -0
  161. package/src/server/chat-completions.ts +55 -2
  162. package/src/server/chat-native.ts +19 -4
  163. package/src/server/claude-messages.ts +55 -17
  164. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  165. package/src/server/gui-freshness.ts +103 -0
  166. package/src/server/gui-static.ts +7 -9
  167. package/src/server/images.ts +59 -6
  168. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  169. package/src/server/index/serve-options.ts +56 -10
  170. package/src/server/index/spend-ledger-lifecycle.ts +34 -8
  171. package/src/server/index/startup-warnings.ts +24 -0
  172. package/src/server/index.ts +21 -28
  173. package/src/server/lifecycle.ts +4 -4
  174. package/src/server/live-call-bindings.ts +6 -0
  175. package/src/server/live.ts +88 -3
  176. package/src/server/management/agent-settings-routes.ts +121 -36
  177. package/src/server/management/companion-routes.ts +77 -0
  178. package/src/server/management/logs-usage-routes.ts +19 -0
  179. package/src/server/management/native-integration-routes.ts +103 -6
  180. package/src/server/management/oauth-account-routes.ts +45 -7
  181. package/src/server/management/route-registry.ts +6 -0
  182. package/src/server/management/shared.ts +18 -1
  183. package/src/server/management/usage-timeline-routes.ts +44 -0
  184. package/src/server/management-api.ts +8 -9
  185. package/src/server/proxy-liveness.ts +75 -0
  186. package/src/server/relay.ts +19 -2
  187. package/src/server/request-log-failure-attribution.ts +99 -0
  188. package/src/server/request-log.ts +114 -0
  189. package/src/server/request-metrics.ts +92 -30
  190. package/src/server/responses/codex-ws-wire.ts +34 -8
  191. package/src/server/responses/combo-stream-preflight.ts +168 -6
  192. package/src/server/responses/compact.ts +11 -0
  193. package/src/server/responses/core-opaque-recovery.ts +90 -0
  194. package/src/server/responses/fetch-helpers.ts +124 -8
  195. package/src/server/responses/input-admission.ts +10 -0
  196. package/src/server/responses/passthrough-delivery.ts +14 -1
  197. package/src/server/responses/passthrough-dispatch.ts +179 -35
  198. package/src/server/responses/passthrough-error.ts +27 -8
  199. package/src/server/responses/request-prepare.ts +42 -1
  200. package/src/server/responses/request-send-budget.ts +12 -0
  201. package/src/server/responses/request-transport.ts +24 -4
  202. package/src/server/responses/reset-replay.ts +108 -0
  203. package/src/server/responses-request-tool-scope.ts +214 -0
  204. package/src/server/responses-undeclared-tool-guard.ts +4 -1
  205. package/src/server/search.ts +25 -1
  206. package/src/server/usage-ledger-retention.ts +73 -0
  207. package/src/service/cli.ts +48 -2
  208. package/src/service/health.ts +3 -2
  209. package/src/service/install-state-contract.d.mts +27 -0
  210. package/src/service/install-state-contract.mjs +34 -0
  211. package/src/service/launchd.ts +1 -1
  212. package/src/service/orchestration.ts +2 -4
  213. package/src/service/ownership-compatibility.ts +164 -0
  214. package/src/service/ownership-mutation-lease.d.mts +32 -0
  215. package/src/service/ownership-mutation-lease.mjs +211 -0
  216. package/src/service/repair.ts +45 -1
  217. package/src/service/state-lock.ts +269 -0
  218. package/src/service/state-record.d.mts +36 -0
  219. package/src/service/state-record.mjs +138 -0
  220. package/src/service/state.ts +582 -68
  221. package/src/service/windows-taskxml.ts +11 -10
  222. package/src/service.ts +7 -3
  223. package/src/tray/windows-tray.ps1 +1 -1
  224. package/src/types/config.ts +37 -0
  225. package/src/types/provider.ts +73 -0
  226. package/src/types/request.ts +28 -2
  227. package/src/types/tools.ts +19 -0
  228. package/src/types.ts +3 -0
  229. package/src/update/index.ts +207 -63
  230. package/src/update/job.ts +9 -5
  231. package/src/update/ownership-transaction.ts +47 -0
  232. package/src/update/restart-ownership.ts +54 -0
  233. package/src/update/runtime-ownership.d.mts +40 -0
  234. package/src/update/runtime-ownership.mjs +122 -0
  235. package/src/usage/attempt-delivery.ts +198 -0
  236. package/src/usage/cache-diagnostic.ts +305 -0
  237. package/src/usage/failure-fingerprint.ts +118 -0
  238. package/src/usage/failure-projection-cache.ts +174 -0
  239. package/src/usage/failure-projection.ts +174 -0
  240. package/src/usage/ledger-retention.ts +165 -0
  241. package/src/usage/log.ts +126 -79
  242. package/src/usage/request-outcome.ts +150 -0
  243. package/src/usage/retention-contract.ts +28 -0
  244. package/src/usage/summary.ts +2 -2
  245. package/src/usage/telemetry-contract.ts +237 -0
  246. package/src/usage/timeline.ts +236 -0
  247. package/src/web-search/alpha-search.ts +21 -1
  248. package/gui/dist/assets/index-BTuCbqQd.css +0 -1
  249. package/gui/dist/assets/index-DoBVdPHP.js +0 -134
package/src/cli/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from "node:child_process";
3
3
  import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { findGuiDist } from "../server/gui-static";
6
+ import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
4
7
 
5
8
  // Best-effort recovery for runtime execution and spawned children if launched
6
9
  // from an unlinked/deleted working directory (runs after hoisted ESM module imports).
@@ -63,8 +66,8 @@ import {
63
66
  pendingTeardownsAreExactly,
64
67
  quarantinePendingTeardown,
65
68
  } from "../config/pending-teardown";
66
- import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
67
- import { endpointsToProve, everyEndpointProvenDown, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
69
+ import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
70
+ import { endpointsToProve, everyEndpointProvenDownAsync, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
68
71
  import { takeFlag } from "./runtime-api";
69
72
  import { parseStartOptions, StartArgsError } from "./start-args";
70
73
 
@@ -82,14 +85,24 @@ import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
82
85
  import { redactUrlForLog } from "../lib/redact";
83
86
  import { dispatchCommand, decideBusyPreferredPort, decideStartWithLiveOwner } from "./dispatch";
84
87
  import { AuxiliaryListenerBindError, findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
85
- import { findLiveProxy, probeHostname, probePortOwner, START_OWNERSHIP_LIVENESS, type LiveProxy } from "../server/proxy-liveness";
88
+ import {
89
+ findLiveProxy,
90
+ probeEndpointLiveness,
91
+ probeHostname,
92
+ probePortOwner,
93
+ START_OWNERSHIP_LIVENESS,
94
+ type LiveProxy,
95
+ } from "../server/proxy-liveness";
86
96
  import { createReadinessGate } from "../server/readiness";
87
97
  import { isApiAuthRequired } from "../server/auth-cors";
88
98
  import { runReady, type ReadyArgs } from "./ready";
99
+ import { runResolve, type ResolveArgs } from "./resolve";
100
+ import { summarizeStopRun, type StopOutcome, type StopRunRecord } from "./stop-report";
89
101
  import { runCli } from "./root";
90
102
  import { isProcessAlive, ProxyOwnershipRefusedError, refusalNextStep, stopProxy } from "../lib/process-control";
91
103
  import { startupDataPlaneToken } from "../lib/service-secrets";
92
- import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxyStillLiveAfterStop, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalledDetailed, uninstallServiceIfInstalled, uninstallServiceDetailed } from "../service";
104
+ import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxyStillLiveAfterStop, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatePaths, serviceStatusSummary, stopServiceIfInstalledDetailed, uninstallServiceIfInstalled, uninstallServiceDetailed } from "../service";
105
+ import { acquireOwnershipMutationLease } from "../service/ownership-mutation-lease.mjs";
93
106
  import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
94
107
  import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
95
108
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
@@ -98,6 +111,10 @@ import { startHistoryMigrationGuardian } from "../codex/history-migration-guardi
98
111
  import { maybeShowStarPrompt } from "./star-prompt";
99
112
  import { scheduleCatalogPrewarm } from "./catalog-prewarm";
100
113
  import { maybeShowUpdatePrompt } from "../update/notify";
114
+ import {
115
+ bindAndPublishStartOwnership,
116
+ StartOwnershipRollbackUncertainError,
117
+ } from "./start-ownership-publication";
101
118
  import { syncModelsToCodex } from "../codex/sync";
102
119
  import {
103
120
  HUB_GATED_SKIP_MESSAGE,
@@ -216,6 +233,13 @@ function startArgv(port?: number): string[] {
216
233
  return selfLaunchArgv(args);
217
234
  }
218
235
 
236
+ class StartCommandExit extends Error {
237
+ constructor(readonly exitCode: number) {
238
+ super(`start command exited with code ${exitCode}`);
239
+ this.name = "StartCommandExit";
240
+ }
241
+ }
242
+
219
243
  async function chooseListenPort(
220
244
  requestedPort?: number,
221
245
  options: { sibling?: boolean } = {},
@@ -288,17 +312,17 @@ async function chooseListenPort(
288
312
  // Same contract as the pre-bind owner check: the wrapper's retry loop terminates
289
313
  // on a zero exit, and the port it was asked to serve is already served.
290
314
  console.log(`Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}); service wrapper staying out of the way.`);
291
- process.exit(0);
315
+ throw new StartCommandExit(0);
292
316
  }
293
317
  if (decision === "refuse-live-proxy") {
294
318
  console.error(`āš ļø Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}). Use 'ocx stop' first.`);
295
- process.exit(1);
319
+ throw new StartCommandExit(1);
296
320
  }
297
321
  if (decision === "refuse-unidentified-holder") {
298
322
  console.error(`āŒ Port ${preferred} is busy and its holder did not identify as opencodex.`);
299
323
  console.error(" Starting on another port would leave Codex pointed at a proxy you did not ask for.");
300
324
  console.error(" Stop whatever holds that port, or start on a free one with 'ocx start --port <port>'.");
301
- process.exit(1);
325
+ throw new StartCommandExit(1);
302
326
  }
303
327
  if (preferred > 0) {
304
328
  console.log(`āš ļø Port ${preferred} is busy; starting opencodex on ${selected}.`);
@@ -313,7 +337,7 @@ async function chooseListenPort(
313
337
  if (err instanceof PortUnavailableError) {
314
338
  console.error(`āŒ ${err.message}`);
315
339
  console.error(" Stop whatever holds that port, or change config.port, then retry.");
316
- process.exit(1);
340
+ throw new StartCommandExit(1);
317
341
  }
318
342
  throw err;
319
343
  }
@@ -443,53 +467,99 @@ async function handleStart(options: { block?: boolean } = {}) {
443
467
  // live daemon holding resources while it overwrites its own binary.
444
468
  await maybeShowUpdatePrompt();
445
469
 
446
- // Port selection is check-then-bind: a concurrent `ocx start`/`ensure` can win the port
447
- // between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
448
- // the same port only (never hop — that was the remaining PR #152 gap).
449
- let port = await chooseListenPort(requestedPort, { sibling: siblingStart });
450
- const { drainAndShutdown, isRecyclingForExit, startServer } = await import("../server");
451
- // One private readiness gate for this startServer invocation, captured by the
452
- // listener's closure. handleStart owns it and transitions it after the
453
- // post-startup sync settles. A second startServer in the same process would
454
- // get its own gate and could never reset/mutate this one.
455
- const readinessGate = createReadinessGate();
456
- let server: ReturnType<typeof startServer>;
457
- const localAttestationSecret = createLocalAttestationSecret();
458
- for (let attempt = 0; ; attempt++) {
459
- try {
460
- server = startServer(port, { localAttestationSecret, readinessGate });
461
- // Prewarm the live provider model cache as soon as the port is bound so the
462
- // first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
463
- // instead of racing duplicate upstream /models fetches.
464
- scheduleCatalogPrewarm();
465
- break;
466
- } catch (err) {
467
- if (err instanceof SpendLedgerOwnerError) {
468
- console.error(`āŒ ${err.message}`);
469
- process.exit(1);
470
- }
471
- if (err instanceof AuxiliaryListenerBindError || !isAddrInUse(err) || attempt >= 2) throw err;
472
- if (requestedPort !== undefined) {
473
- console.log(`āš ļø Port ${port} was taken while starting; waiting to retry the same port...`);
474
- const hostname = loadConfig().hostname ?? "127.0.0.1";
475
- const freed = await waitForPortAvailable(port, hostname, { timeoutMs: 3_000, intervalMs: 50 });
476
- if (!freed) {
477
- console.error(`āŒ Port ${port} stayed busy; refusing to hop to an ephemeral port.`);
478
- process.exit(1);
470
+ type StartServerModule = typeof import("../server");
471
+ type BoundStart = {
472
+ server: ReturnType<StartServerModule["startServer"]>;
473
+ serverModule: StartServerModule;
474
+ port: number;
475
+ readinessGate: ReturnType<typeof createReadinessGate>;
476
+ localAttestationSecret: string;
477
+ config: ReturnType<typeof loadConfig>;
478
+ };
479
+ let boundStart: BoundStart;
480
+ try {
481
+ boundStart = await bindAndPublishStartOwnership({
482
+ acquireLease: () => acquireOwnershipMutationLease(serviceStatePaths()),
483
+ bind: async () => {
484
+ // The earlier probe owned journal cleanup. This one owns the bind decision: an
485
+ // updater may have stopped the old runtime and acquired this lease for replacement.
486
+ const fencedLive = await findLiveProxy(START_OWNERSHIP_LIVENESS);
487
+ if (fencedLive) {
488
+ const decision = decideStartWithLiveOwner({
489
+ livePort: fencedLive.port,
490
+ requestedPort,
491
+ ocxService: process.env.OCX_SERVICE,
492
+ });
493
+ if (decision === "service-stay-out") {
494
+ console.log(`Proxy already running (PID ${fencedLive.pid ?? "unknown"}, port ${fencedLive.port}); service wrapper staying out of the way.`);
495
+ throw new StartCommandExit(0);
496
+ }
497
+ if (decision === "refuse") {
498
+ console.error(`āš ļø Proxy appeared before bind (PID ${fencedLive.pid ?? "unknown"}, port ${fencedLive.port}). Use 'ocx stop' first.`);
499
+ throw new StartCommandExit(1);
500
+ }
501
+ siblingStart = true;
479
502
  }
480
- continue;
481
- }
482
- console.log(`āš ļø Port ${port} was taken while starting; picking another...`);
483
- port = await chooseListenPort(requestedPort, { sibling: siblingStart });
484
- }
503
+
504
+ // Port selection is check-then-bind. The lease prevents every cooperating start or
505
+ // updater from turning that check into a different ownership decision.
506
+ let port = await chooseListenPort(requestedPort, { sibling: siblingStart });
507
+ const serverModule = await import("../server");
508
+ const readinessGate = createReadinessGate();
509
+ const localAttestationSecret = createLocalAttestationSecret();
510
+ const config = loadConfig();
511
+ let server: ReturnType<typeof serverModule.startServer>;
512
+ for (let attempt = 0; ; attempt++) {
513
+ try {
514
+ server = serverModule.startServer(port, { localAttestationSecret, readinessGate });
515
+ break;
516
+ } catch (err) {
517
+ try { await serverModule.waitForFailedStartRollback(err); }
518
+ catch (rollbackError) {
519
+ throw new StartOwnershipRollbackUncertainError([err, rollbackError]);
520
+ }
521
+ if (err instanceof SpendLedgerOwnerError) {
522
+ console.error(`āŒ ${err.message}`);
523
+ throw new StartCommandExit(1);
524
+ }
525
+ if (err instanceof AuxiliaryListenerBindError || !isAddrInUse(err) || attempt >= 2) throw err;
526
+ if (requestedPort !== undefined) {
527
+ console.log(`āš ļø Port ${port} was taken while starting; waiting to retry the same port...`);
528
+ const hostname = config.hostname ?? "127.0.0.1";
529
+ const freed = await waitForPortAvailable(port, hostname, { timeoutMs: 3_000, intervalMs: 50 });
530
+ if (!freed) {
531
+ console.error(`āŒ Port ${port} stayed busy; refusing to hop to an ephemeral port.`);
532
+ throw new StartCommandExit(1);
533
+ }
534
+ continue;
535
+ }
536
+ console.log(`āš ļø Port ${port} was taken while starting; picking another...`);
537
+ port = await chooseListenPort(requestedPort, { sibling: siblingStart });
538
+ }
539
+ }
540
+ return { server, serverModule, port, readinessGate, localAttestationSecret, config };
541
+ },
542
+ writePid: () => writePid(process.pid),
543
+ writeRuntime: bound => writeRuntimePort({
544
+ pid: process.pid,
545
+ port: bound.port,
546
+ hostname: bound.config.hostname,
547
+ attestationSecret: bound.localAttestationSecret,
548
+ }),
549
+ stopBound: bound => bound.server.stop(true),
550
+ removeRuntime: () => removeRuntimePortIfPidIs(process.pid),
551
+ removePid: () => removePidIfValueIs(process.pid),
552
+ });
553
+ } catch (error) {
554
+ if (error instanceof StartCommandExit) { process.exitCode = error.exitCode; return; }
555
+ throw error;
485
556
  }
486
- // A single request's streaming error must never crash the daemon serving every
487
- // other Codex session — capture the full stack to crash.log and stay up.
488
- installCrashGuards();
489
- writePid(process.pid);
490
557
 
491
- const config = loadConfig();
492
- writeRuntimePort({ pid: process.pid, port, hostname: config.hostname, attestationSecret: localAttestationSecret });
558
+ const { server, serverModule, port, readinessGate, config } = boundStart;
559
+ const { drainAndShutdown, isRecyclingForExit } = serverModule;
560
+ // Records are visible now; background work may observe this runtime without a gap.
561
+ scheduleCatalogPrewarm();
562
+ installCrashGuards();
493
563
  // No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
494
564
  // is about to transform; snapshotting earlier only captured a baseline that could
495
565
  // already be stale by the time injection ran (#477).
@@ -597,11 +667,11 @@ async function handleStart(options: { block?: boolean } = {}) {
597
667
  try {
598
668
  const { fetchAllModels } = await import("../server/management-api");
599
669
  const { desktopVisibleNativeSlugs } = await import("../codex/catalog");
600
- const { resolveCodexModelEntitlements } = await import("../codex/model-entitlements");
670
+ const { resolveAdmittedCodexModelEntitlements } = await import("../codex/model-entitlement-admission");
601
671
  const { buildDesktopDiscoveryInputs } = await import("../claude/desktop-discovery-inputs");
602
672
  const [models, modelEntitlements] = await Promise.all([
603
673
  fetchAllModels(config),
604
- resolveCodexModelEntitlements(config, { clientVersion: null }),
674
+ resolveAdmittedCodexModelEntitlements(config, { clientVersion: null }),
605
675
  ]);
606
676
  const inputs = buildDesktopDiscoveryInputs({
607
677
  config, models, modelEntitlements,
@@ -921,6 +991,12 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
921
991
  }
922
992
 
923
993
  async function handleStop() {
994
+ const lease = acquireOwnershipMutationLease(serviceStatePaths());
995
+ try { return await handleStopUnlocked(); }
996
+ finally { lease.release(); }
997
+ }
998
+
999
+ async function handleStopUnlocked() {
924
1000
  // The receipt must name the endpoint the owner was stopping — an obligation nobody can
925
1001
  // locate cannot be proven discharged. Only the runtime record knows it; a proxy started
926
1002
  // with an explicit --port is not on the configured one.
@@ -952,8 +1028,7 @@ async function handleStop() {
952
1028
  // An obligation that cannot name its endpoint cannot be proven discharged.
953
1029
  if (!endpoint) return false;
954
1030
  try {
955
- const { probeProxyLiveness } = await import("../update/proxy-liveness-probe.mjs");
956
- return probeProxyLiveness(endpoint.port, endpoint.hostname) === "dead";
1031
+ return await probeEndpointLiveness(endpoint) === "dead";
957
1032
  } catch {
958
1033
  // A probe that could not run is not evidence of absence.
959
1034
  return false;
@@ -977,6 +1052,16 @@ async function handleStop() {
977
1052
  // service — the exact failure this flag prevents. A plain stop failure is different: we
978
1053
  // tried, so local teardown still proceeds.
979
1054
  let ownershipBlocked = false;
1055
+ // Structured twin of the human lines below, for `ocx stop --json`: one document the
1056
+ // desktop shell can read across the process boundary (D4). Every field is assigned
1057
+ // where the corresponding boolean already flips — the summarizer never re-decides.
1058
+ const record: StopRunRecord = {
1059
+ service: "absent",
1060
+ proxy: "unknown",
1061
+ sharedTeardown: "skipped",
1062
+ inheritedTeardownBlocks: false,
1063
+ receiptClearFailed: false,
1064
+ };
980
1065
  // Deferring shared teardown to this process is an obligation, so record it on disk
981
1066
  // before asking for it (#3008). A parent that dies mid-stop would otherwise leave the
982
1067
  // client config routed at a proxy that is already gone, with nothing to find later.
@@ -1054,6 +1139,7 @@ async function handleStop() {
1054
1139
  };
1055
1140
  try {
1056
1141
  const serviceStop = stopServiceIfInstalledDetailed();
1142
+ record.service = serviceStop;
1057
1143
  stoppedService = serviceStop === "stopped" || serviceStop === "stopped-respawnable";
1058
1144
  schedulerCanRespawn = serviceStop === "stopped-respawnable";
1059
1145
  // No "won't respawn" claim here: a stopped Task Scheduler can still respawn through
@@ -1074,6 +1160,7 @@ async function handleStop() {
1074
1160
  console.error(" Run 'ocx service status' to see the query error, repair Task Scheduler access, then retry.");
1075
1161
  }
1076
1162
  } catch (err) {
1163
+ record.service = "error";
1077
1164
  if (isServiceOwnershipError(err)) {
1078
1165
  ownershipBlocked = true;
1079
1166
  stopFailed = true;
@@ -1097,8 +1184,10 @@ async function handleStop() {
1097
1184
  console.log(`āœ… Proxy (PID ${pid}) stopped.`);
1098
1185
  removePid(pid);
1099
1186
  removeRuntimePort(pid);
1187
+ record.proxy = "stopped";
1100
1188
  } catch (err) {
1101
1189
  stopFailed = true;
1190
+ record.proxy = err instanceof ProxyOwnershipRefusedError ? "ownership-refused" : "stop-failed";
1102
1191
  console.error(`āŒ Failed to stop proxy (PID ${pid}).`);
1103
1192
  // stopProxy throws with the reason — an ownership refusal (409) carries the
1104
1193
  // remediation ("run the stop from that home"). Swallowing it leaves the operator
@@ -1133,8 +1222,10 @@ async function handleStop() {
1133
1222
  { hostname: live.hostname ?? "127.0.0.1", port: live.port },
1134
1223
  );
1135
1224
  console.log(`āœ… Proxy (PID ${live.pid}) stopped.`);
1225
+ record.proxy = "stopped-orphan";
1136
1226
  } catch (err) {
1137
1227
  stopFailed = true;
1228
+ record.proxy = err instanceof ProxyOwnershipRefusedError ? "ownership-refused" : "stop-failed";
1138
1229
  console.error(`āŒ Failed to stop proxy (PID ${live.pid}).`);
1139
1230
  const detail = err instanceof Error ? err.message : String(err);
1140
1231
  if (detail) console.error(` ${detail}`);
@@ -1152,12 +1243,14 @@ async function handleStop() {
1152
1243
  // under a proxy that is still serving — the exact failure the deferral exists to
1153
1244
  // prevent, arrived at from the other direction.
1154
1245
  stopFailed = true;
1246
+ record.proxy = "unresolvable-pid";
1155
1247
  ownershipBlocked = true;
1156
1248
  console.error(`āŒ A proxy is answering on port ${live.port}, but no process id could be resolved for it, so it cannot be stopped from here.`);
1157
1249
  console.error(" Skipping shared teardown: restoring client config while it serves would leave both pointing at each other.");
1158
1250
  console.error(" Stop it from the home that started it, or end the process manually, then rerun 'ocx stop'.");
1159
- } else if (!stoppedService) {
1160
- console.log("No running proxy found.");
1251
+ } else {
1252
+ record.proxy = "not-running";
1253
+ if (!stoppedService) console.log("No running proxy found.");
1161
1254
  }
1162
1255
  if (!stopFailed) {
1163
1256
  // `readPid() === null` means the snapshotted pid file was absent, invalid, dead, or
@@ -1180,6 +1273,7 @@ async function handleStop() {
1180
1273
  const survivor = await proxyStillLiveAfterStop({ canRespawn: true });
1181
1274
  if (survivor) {
1182
1275
  stopFailed = true;
1276
+ record.proxy = "respawned";
1183
1277
  console.error(`āŒ A proxy is still listening on port ${survivor.port} after the service stop; it is being respawned.`);
1184
1278
  console.error(" Skipping shared teardown: restoring client config while the proxy runs leaves both pointing at each other.");
1185
1279
  ownershipBlocked = true;
@@ -1215,6 +1309,7 @@ async function handleStop() {
1215
1309
  // No file, no nonce: nothing to quarantine and nothing to remove. The home itself
1216
1310
  // may be hiding an obligation, so block and ask for the directory to be fixed.
1217
1311
  inheritedBlocks = true;
1312
+ record.inheritedTeardownBlocks = true;
1218
1313
  stopFailed = true;
1219
1314
  console.error(`āŒ ${read.detail}, so this stop cannot tell whether a shared teardown is still owed.`);
1220
1315
  console.error(" Skipping shared teardown. Fix access to the opencodex home, then rerun 'ocx stop'.");
@@ -1223,6 +1318,7 @@ async function handleStop() {
1223
1318
  if (read.state === "invalid") {
1224
1319
  unreadable.push(read);
1225
1320
  inheritedBlocks = true;
1321
+ record.inheritedTeardownBlocks = true;
1226
1322
  stopFailed = true;
1227
1323
  console.error(`āŒ A pending-teardown receipt could not be read (${read.detail}).`);
1228
1324
  console.error(" It names no endpoint, so this stop cannot prove the proxy it belonged to is down.");
@@ -1234,6 +1330,7 @@ async function handleStop() {
1234
1330
  // proxy on an explicit --port can be respawned there while this address refuses,
1235
1331
  // so "dead" here proves nothing and must not authorize a restore.
1236
1332
  inheritedBlocks = true;
1333
+ record.inheritedTeardownBlocks = true;
1237
1334
  stopFailed = true;
1238
1335
  console.error("āŒ A shared teardown from an earlier stop is outstanding, but that stop could not record the address it was stopping.");
1239
1336
  console.error(` Only the configured address (${read.receipt.endpoint.hostname}:${read.receipt.endpoint.port}) was recorded, which cannot prove the right proxy is down.`);
@@ -1245,12 +1342,14 @@ async function handleStop() {
1245
1342
  continue;
1246
1343
  }
1247
1344
  inheritedBlocks = true;
1345
+ record.inheritedTeardownBlocks = true;
1248
1346
  stopFailed = true;
1249
1347
  console.error(`āŒ A shared teardown from an earlier stop is still outstanding, and the proxy on ${read.receipt.endpoint.hostname}:${read.receipt.endpoint.port} could not be confirmed down.`);
1250
1348
  console.error(" Skipping shared teardown: restoring client config under a proxy that may still be running is what the deferral exists to prevent.");
1251
1349
  console.error(" The obligation is preserved; retry once the proxy is confirmed stopped.");
1252
1350
  }
1253
1351
  }
1352
+ if (nativeRestoreHandledByProxy) record.sharedTeardown = "performed-by-proxy";
1254
1353
  const restoreBlocked = ownershipBlocked || inheritedBlocks || nativeRestoreHandledByProxy;
1255
1354
  if (!restoreBlocked) {
1256
1355
  if (recoveredNonces.length > 0) {
@@ -1259,6 +1358,7 @@ async function handleStop() {
1259
1358
  console.log("ā†©ļø Finishing a shared teardown left unfinished by an earlier stop.");
1260
1359
  }
1261
1360
  const restore = await restoreSharedClientStateAfterStop();
1361
+ record.sharedTeardown = restore.historyDeferred ? "refused" : restore.other ? "failed" : "restored";
1262
1362
  if (restore.other) stopFailed = true;
1263
1363
  else if (restore.historyDeferred) historyDeferredNonces = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
1264
1364
  else if (restore.historyOnly) historyOnlyFailure = true;
@@ -1284,6 +1384,7 @@ async function handleStop() {
1284
1384
  // removal is surfaced rather than swallowed.
1285
1385
  if (!clearPendingTeardown(nonce)) {
1286
1386
  stopFailed = true;
1387
+ record.receiptClearFailed = true;
1287
1388
  console.error(`āŒ The shared teardown finished, but its receipt could not be removed: ${pendingTeardownPathFor(nonce)}`);
1288
1389
  console.error(" Remove it manually; otherwise every later stop and update will try to recover it again.");
1289
1390
  }
@@ -1328,18 +1429,23 @@ async function handleStop() {
1328
1429
  ? STOP_HISTORY_DEFERRED_EXIT_CODE
1329
1430
  : 1;
1330
1431
  }
1331
- return !stopFailed;
1432
+ const summary = summarizeStopRun(record, {
1433
+ failed: stopFailed,
1434
+ historyOnly: historyOnlyFailure,
1435
+ historyDeferred: historyDeferredNonces !== null,
1436
+ exitCode: Number(process.exitCode ?? 0),
1437
+ });
1438
+ return { ok: !stopFailed, summary };
1332
1439
  }
1333
1440
 
1334
1441
  async function handleUninstall() {
1335
1442
  /** Definitive "nothing is answering" on the endpoint this home would serve. */
1336
1443
  const proxyEndpointProvenDown = async (): Promise<boolean> => {
1337
1444
  try {
1338
- const { probeProxyLiveness } = await import("../update/proxy-liveness-probe.mjs");
1339
1445
  // Every candidate, not just the preferred one: a stale runtime record pointing at a
1340
1446
  // closed port would otherwise "prove" a live proxy on the configured port is gone.
1341
1447
  const endpoints = endpointsToProve(readRuntimePort(), loadConfig());
1342
- return everyEndpointProvenDown(endpoints, e => probeProxyLiveness(e.port, e.hostname));
1448
+ return await everyEndpointProvenDownAsync(endpoints, probeEndpointLiveness);
1343
1449
  } catch {
1344
1450
  return false;
1345
1451
  }
@@ -1586,8 +1692,26 @@ async function handleStatus() {
1586
1692
  console.log(installed
1587
1693
  ? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
1588
1694
  : " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
1695
+ // Restarting is only half the choice. A user who cannot sign in to Codex at all needs the
1696
+ // way out that does not require this proxy to come back (#5261).
1697
+ for (const line of deadProxyRoutingAdviceLines({
1698
+ proxyUp: false,
1699
+ routingKind: status.json.startup.routingKind,
1700
+ })) {
1701
+ console.log(` ${line}`);
1702
+ }
1589
1703
  }
1590
1704
  console.log(` Dashboard: ${status.json.dashboard.url}${local}`);
1705
+ // The dashboard is a build artifact, so a checkout that moved without `bun run build:gui` keeps
1706
+ // serving the previous bundle and every feature added since simply does not appear (#5196's
1707
+ // usage panel was invisible this way for five days). Reported next to the dashboard URL, which
1708
+ // is where someone looks when the page is wrong.
1709
+ for (const line of staleGuiBundleLines(inspectGuiBundleFreshness({
1710
+ bundlePath: findGuiDist(),
1711
+ sourcePath: join(import.meta.dir, "..", "..", "gui", "src"),
1712
+ }))) {
1713
+ console.log(` ${line}`);
1714
+ }
1591
1715
  console.log(` Config: ${status.json.paths.config}${local}`);
1592
1716
  console.log(` PID file: ${status.json.paths.pid}${local}`);
1593
1717
  console.log(` Runtime: ${status.json.paths.runtime}${local}`);
@@ -1606,6 +1730,12 @@ async function handleStatus() {
1606
1730
  console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
1607
1731
  console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
1608
1732
  console.log(` ${formatStartupRoutingDetail(status.json.startup)}${local}`);
1733
+ // Independent of whether the proxy is up: a catalog pointer whose file is gone stops Codex
1734
+ // loading its config at all, and presents as the same blank wall as dead routing (#5261).
1735
+ // Tagged `(local)` on its header like every other local-state line, so a connected client
1736
+ // cannot read a finding about its own Codex home as something the hub reported.
1737
+ missingCodexCatalogLines(detectMissingCodexCatalogPath())
1738
+ .forEach((line, index) => console.log(` ${line}${index === 0 ? local : ""}`));
1609
1739
  if (status.json.startup.routingKind === "native") {
1610
1740
  let retainedProviderTable = false;
1611
1741
  try {
@@ -1734,6 +1864,14 @@ async function handleReady(args: ReadyArgs): Promise<number> {
1734
1864
  return runReady(args);
1735
1865
  }
1736
1866
 
1867
+ /**
1868
+ * `ocx resolve` — argument parsing already happened in src/cli/root.ts (before any
1869
+ * preflight side effect), so this handler only runs the runner and returns its exit code.
1870
+ */
1871
+ async function handleResolve(args: ResolveArgs): Promise<number> {
1872
+ return runResolve(args);
1873
+ }
1874
+
1737
1875
  process.exit(await dispatchCommand(head, {
1738
1876
  args,
1739
1877
  command,
@@ -1754,6 +1892,7 @@ process.exit(await dispatchCommand(head, {
1754
1892
  },
1755
1893
  handleStart,
1756
1894
  handleStop,
1895
+ handleResolve,
1757
1896
  handleEnsure,
1758
1897
  handleTrayProxyStart,
1759
1898
  handleTrayProxyRestart,
package/src/cli/init.ts CHANGED
@@ -253,6 +253,14 @@ export async function runInit(): Promise<void> {
253
253
  }
254
254
 
255
255
  console.log(`\nšŸš€ Setup complete! Run 'ocx start' to start the proxy.`);
256
+ // Said after the autostart choice, because the choice is what decides whether it applies.
257
+ // Setup otherwise ends on a success line while leaving a restart dependency unmentioned.
258
+ try {
259
+ const { collectStartupHealth, injectedRoutingRestartWarningLines } = await import("../codex/autostart-health");
260
+ for (const line of injectedRoutingRestartWarningLines(collectStartupHealth(config))) console.log(line);
261
+ } catch {
262
+ // A diagnostic that cannot be computed must not fail a completed setup.
263
+ }
256
264
  for (const line of modelSelectionGuidance(providerName)) console.log(line);
257
265
  } catch (error) {
258
266
  if (error instanceof InitCancelledError) {
@@ -229,7 +229,13 @@ export async function handleClientIntegrationCommand(
229
229
  ? profiles.map(row => `${String(row.profileId)} ${String(row.name ?? "Aside")}: ${row.enabled ? "on" : "off"} (${String(row.state)})${row.current ? " [current]" : ""}`)
230
230
  : [String((result as { error?: string }).error ?? "No Aside profiles found.")]
231
231
  : rows
232
- ? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}`)
232
+ /*
233
+ * `supersededBy` is named here and not only in the single-client view
234
+ * because this list is where a user looks to see that everything is
235
+ * connected, and "current" alone is exactly the reassurance that hid a
236
+ * client reading a file opencodex does not write.
237
+ */
238
+ ? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}${row.supersededBy ? " (client reads another file)" : ""}`)
233
239
  : singleClientStatusLines(result));
234
240
  return;
235
241
  }
@@ -29,18 +29,32 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
29
29
  "--socks5-off Clear a saved SOCKS5 outbound proxy from config.proxy.",
30
30
  ],
31
31
  },
32
- { name: "stop", usage: "ocx stop", summary: "Stop the proxy and restore native Codex config." },
32
+ {
33
+ name: "stop",
34
+ usage: "ocx stop [--json]",
35
+ summary: "Stop the proxy and restore native Codex config.",
36
+ details: [
37
+ "--json keeps the stop path unchanged and prints one structured summary document on stdout; human output moves to stderr.",
38
+ "Exit codes are identical with and without --json: 0, 1, 79 (history cleanup incomplete), 80 (teardown deferred).",
39
+ ],
40
+ },
33
41
  {
34
42
  name: "restore",
35
43
  aliases: ["eject"],
36
44
  usage: "ocx restore [back]",
37
45
  summary: "Restore native Codex config without stopping the proxy; `restore back` re-points codex at the running proxy.",
46
+ details: [
47
+ "--remove-codex-provider-table Also remove [model_providers.opencodex] when a paginated home made restore keep it. Conversations tagged opencodex stop opening.",
48
+ ],
38
49
  },
39
50
  {
40
51
  name: "eject",
41
52
  aliases: [],
42
53
  usage: "ocx eject [back]",
43
54
  summary: "Restore native Codex config without stopping the proxy; `eject back` re-points codex at the running proxy.",
55
+ details: [
56
+ "--remove-codex-provider-table Also remove [model_providers.opencodex] when a paginated home made restore keep it. Conversations tagged opencodex stop opening.",
57
+ ],
44
58
  },
45
59
  {
46
60
  name: "recover-history",
@@ -94,9 +108,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
94
108
  {
95
109
  name: "tray",
96
110
  usage: "ocx tray <install|start|stop|status|uninstall|remove> [--json] [--no-start]",
97
- summary: "Install and control the Windows status tray icon.",
111
+ summary: "Install and control the Windows status tray icon (deprecated in favor of the desktop app).",
98
112
  details: [
99
113
  "The tray starts at Windows login and provides one-click proxy controls.",
114
+ "Deprecated: the OpenCodex desktop app provides the tray on Windows, macOS, and Linux; `ocx tray` remains for installs without the desktop app.",
100
115
  "Tray start/stop controls the icon only; use its menu to start or stop the proxy.",
101
116
  "--no-start (install only) installs the tray without launching it immediately.",
102
117
  ],
@@ -298,6 +313,16 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
298
313
  usage: "ocx model <subcommand>",
299
314
  summary: "Alias of ocx models.",
300
315
  },
316
+ {
317
+ name: "companion",
318
+ usage: "ocx companion <show|set|reset> ...",
319
+ summary: "Inspect and configure menu-bar and widget companion usage settings.",
320
+ details: [
321
+ "ocx companion and ocx companion show read settings; use --json for machine-readable output.",
322
+ "ocx companion set accepts one or more key=value assignments; values are parsed as JSON when possible.",
323
+ "ocx companion reset restores the default settings.",
324
+ ],
325
+ },
301
326
  {
302
327
  name: "combo",
303
328
  usage: "ocx combo <list|show|set|remove> ...",
@@ -480,6 +505,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
480
505
  details: [
481
506
  "Alias of ocx integration client <sub> --client zcode.",
482
507
  "enable writes the managed provider.opencodex block into ~/.zcode/v2/config.json; disable removes only that block.",
508
+ "ZCode 3.14 moved its providers to ~/.zcode/v2/provider_config.json; where that file exists, enable is refused because the write cannot reach the client.",
483
509
  "ZCode reads its config at startup — restart ZCode after enable/disable.",
484
510
  "Select OpenCodex Proxy/<provider>/<model> from ZCode's model picker.",
485
511
  ],
@@ -531,6 +557,19 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
531
557
  "Invalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.",
532
558
  ],
533
559
  },
560
+ {
561
+ name: "resolve",
562
+ usage: "ocx resolve [--json]",
563
+ summary: "Emit the resolved config home, effective port, and identity-checked proxy liveness as one JSON document.",
564
+ details: [
565
+ "Machine surface for embedding shells: it replaces a second home/port/liveness implementation beside the CLI.",
566
+ "The port is the live listener's port when an opencodex proxy answers, otherwise the configured port (default 10100).",
567
+ "Liveness is three-valued: live, absent-proven (every recorded and configured endpoint definitively dead), or unknown — unknown exits 1 and never reads as absent.",
568
+ "--json emits one versioned document (schema ocx-resolve/1); the default prints two human lines.",
569
+ "Exit 0 carries a trustworthy verdict; exit 1 means the CLI could not resolve (invalid config or undecidable liveness) and callers must refuse to guess.",
570
+ "Any unknown argument exits 64 before preflight side effects.",
571
+ ],
572
+ },
534
573
  {
535
574
  name: "lab",
536
575
  usage: "ocx lab <status|verdicts|subjects|subject|observations|events|event|artifacts|artifact|catalog> [options] [--json]",