@bitkyc08/opencodex 2.59.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 (374) 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 +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/cli/hub.ts CHANGED
@@ -228,8 +228,9 @@ export function hubInviteCommand(
228
228
  managementUrl: string,
229
229
  clients: OcxConnectedClientId[],
230
230
  ): string {
231
- const clientsFlag = clients.length > 0 ? ` --clients ${clients.join(",")}` : "";
232
- return `echo '${code}' | ocx connect ${dataUrl} --management-url ${managementUrl}${clientsFlag} --pairing-code-stdin`;
231
+ const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
232
+ const clientsFlag = clients.length > 0 ? ` --clients ${shellQuote(clients.join(","))}` : "";
233
+ return `echo ${shellQuote(code)} | ocx connect ${shellQuote(dataUrl)} --management-url ${shellQuote(managementUrl)}${clientsFlag} --pairing-code-stdin`;
233
234
  }
234
235
 
235
236
  async function runInvite(args: string[], deps: HubCommandDeps): Promise<number> {
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
 
@@ -78,17 +81,28 @@ import {
78
81
  } from "./tray-proxy";
79
82
  import { requestBoundSystemRestart } from "./system-restart-client";
80
83
  import { installCrashGuards } from "../lib/crash-guard";
84
+ import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
81
85
  import { redactUrlForLog } from "../lib/redact";
82
86
  import { dispatchCommand, decideBusyPreferredPort, decideStartWithLiveOwner } from "./dispatch";
83
87
  import { AuxiliaryListenerBindError, findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
84
- 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";
85
96
  import { createReadinessGate } from "../server/readiness";
86
97
  import { isApiAuthRequired } from "../server/auth-cors";
87
98
  import { runReady, type ReadyArgs } from "./ready";
99
+ import { runResolve, type ResolveArgs } from "./resolve";
100
+ import { summarizeStopRun, type StopOutcome, type StopRunRecord } from "./stop-report";
88
101
  import { runCli } from "./root";
89
102
  import { isProcessAlive, ProxyOwnershipRefusedError, refusalNextStep, stopProxy } from "../lib/process-control";
90
103
  import { startupDataPlaneToken } from "../lib/service-secrets";
91
- 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";
92
106
  import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
93
107
  import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
94
108
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
@@ -97,6 +111,10 @@ import { startHistoryMigrationGuardian } from "../codex/history-migration-guardi
97
111
  import { maybeShowStarPrompt } from "./star-prompt";
98
112
  import { scheduleCatalogPrewarm } from "./catalog-prewarm";
99
113
  import { maybeShowUpdatePrompt } from "../update/notify";
114
+ import {
115
+ bindAndPublishStartOwnership,
116
+ StartOwnershipRollbackUncertainError,
117
+ } from "./start-ownership-publication";
100
118
  import { syncModelsToCodex } from "../codex/sync";
101
119
  import {
102
120
  HUB_GATED_SKIP_MESSAGE,
@@ -215,6 +233,13 @@ function startArgv(port?: number): string[] {
215
233
  return selfLaunchArgv(args);
216
234
  }
217
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
+
218
243
  async function chooseListenPort(
219
244
  requestedPort?: number,
220
245
  options: { sibling?: boolean } = {},
@@ -287,17 +312,17 @@ async function chooseListenPort(
287
312
  // Same contract as the pre-bind owner check: the wrapper's retry loop terminates
288
313
  // on a zero exit, and the port it was asked to serve is already served.
289
314
  console.log(`Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}); service wrapper staying out of the way.`);
290
- process.exit(0);
315
+ throw new StartCommandExit(0);
291
316
  }
292
317
  if (decision === "refuse-live-proxy") {
293
318
  console.error(`āš ļø Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}). Use 'ocx stop' first.`);
294
- process.exit(1);
319
+ throw new StartCommandExit(1);
295
320
  }
296
321
  if (decision === "refuse-unidentified-holder") {
297
322
  console.error(`āŒ Port ${preferred} is busy and its holder did not identify as opencodex.`);
298
323
  console.error(" Starting on another port would leave Codex pointed at a proxy you did not ask for.");
299
324
  console.error(" Stop whatever holds that port, or start on a free one with 'ocx start --port <port>'.");
300
- process.exit(1);
325
+ throw new StartCommandExit(1);
301
326
  }
302
327
  if (preferred > 0) {
303
328
  console.log(`āš ļø Port ${preferred} is busy; starting opencodex on ${selected}.`);
@@ -312,7 +337,7 @@ async function chooseListenPort(
312
337
  if (err instanceof PortUnavailableError) {
313
338
  console.error(`āŒ ${err.message}`);
314
339
  console.error(" Stop whatever holds that port, or change config.port, then retry.");
315
- process.exit(1);
340
+ throw new StartCommandExit(1);
316
341
  }
317
342
  throw err;
318
343
  }
@@ -418,8 +443,8 @@ async function handleStart(options: { block?: boolean } = {}) {
418
443
  // to bake the service (observed: a probe on 10198 left the service pinned there).
419
444
  siblingStart = true;
420
445
  console.warn(
421
- `Proxy already running on port ${owner.live.port}; starting a second instance on requested port ${requestedPort}. `
422
- + `The new instance takes over this home's pid/runtime records and Codex config while it runs.`,
446
+ `Proxy already running on port ${owner.live.port}; requested a second instance on port ${requestedPort}. `
447
+ + `Startup continues only for an independent OPENCODEX_HOME; one state directory has one spend-ledger writer.`,
423
448
  );
424
449
  }
425
450
 
@@ -442,49 +467,99 @@ async function handleStart(options: { block?: boolean } = {}) {
442
467
  // live daemon holding resources while it overwrites its own binary.
443
468
  await maybeShowUpdatePrompt();
444
469
 
445
- // Port selection is check-then-bind: a concurrent `ocx start`/`ensure` can win the port
446
- // between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
447
- // the same port only (never hop — that was the remaining PR #152 gap).
448
- let port = await chooseListenPort(requestedPort, { sibling: siblingStart });
449
- const { drainAndShutdown, isRecyclingForExit, startServer } = await import("../server");
450
- // One private readiness gate for this startServer invocation, captured by the
451
- // listener's closure. handleStart owns it and transitions it after the
452
- // post-startup sync settles. A second startServer in the same process would
453
- // get its own gate and could never reset/mutate this one.
454
- const readinessGate = createReadinessGate();
455
- let server: ReturnType<typeof startServer>;
456
- const localAttestationSecret = createLocalAttestationSecret();
457
- for (let attempt = 0; ; attempt++) {
458
- try {
459
- server = startServer(port, { localAttestationSecret, readinessGate });
460
- // Prewarm the live provider model cache as soon as the port is bound so the
461
- // first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
462
- // instead of racing duplicate upstream /models fetches.
463
- scheduleCatalogPrewarm();
464
- break;
465
- } catch (err) {
466
- if (err instanceof AuxiliaryListenerBindError || !isAddrInUse(err) || attempt >= 2) throw err;
467
- if (requestedPort !== undefined) {
468
- console.log(`āš ļø Port ${port} was taken while starting; waiting to retry the same port...`);
469
- const hostname = loadConfig().hostname ?? "127.0.0.1";
470
- const freed = await waitForPortAvailable(port, hostname, { timeoutMs: 3_000, intervalMs: 50 });
471
- if (!freed) {
472
- console.error(`āŒ Port ${port} stayed busy; refusing to hop to an ephemeral port.`);
473
- 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;
474
502
  }
475
- continue;
476
- }
477
- console.log(`āš ļø Port ${port} was taken while starting; picking another...`);
478
- port = await chooseListenPort(requestedPort, { sibling: siblingStart });
479
- }
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;
480
556
  }
481
- // A single request's streaming error must never crash the daemon serving every
482
- // other Codex session — capture the full stack to crash.log and stay up.
483
- installCrashGuards();
484
- writePid(process.pid);
485
557
 
486
- const config = loadConfig();
487
- 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();
488
563
  // No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
489
564
  // is about to transform; snapshotting earlier only captured a baseline that could
490
565
  // already be stale by the time injection ran (#477).
@@ -592,11 +667,11 @@ async function handleStart(options: { block?: boolean } = {}) {
592
667
  try {
593
668
  const { fetchAllModels } = await import("../server/management-api");
594
669
  const { desktopVisibleNativeSlugs } = await import("../codex/catalog");
595
- const { resolveCodexModelEntitlements } = await import("../codex/model-entitlements");
670
+ const { resolveAdmittedCodexModelEntitlements } = await import("../codex/model-entitlement-admission");
596
671
  const { buildDesktopDiscoveryInputs } = await import("../claude/desktop-discovery-inputs");
597
672
  const [models, modelEntitlements] = await Promise.all([
598
673
  fetchAllModels(config),
599
- resolveCodexModelEntitlements(config, { clientVersion: null }),
674
+ resolveAdmittedCodexModelEntitlements(config, { clientVersion: null }),
600
675
  ]);
601
676
  const inputs = buildDesktopDiscoveryInputs({
602
677
  config, models, modelEntitlements,
@@ -916,6 +991,12 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
916
991
  }
917
992
 
918
993
  async function handleStop() {
994
+ const lease = acquireOwnershipMutationLease(serviceStatePaths());
995
+ try { return await handleStopUnlocked(); }
996
+ finally { lease.release(); }
997
+ }
998
+
999
+ async function handleStopUnlocked() {
919
1000
  // The receipt must name the endpoint the owner was stopping — an obligation nobody can
920
1001
  // locate cannot be proven discharged. Only the runtime record knows it; a proxy started
921
1002
  // with an explicit --port is not on the configured one.
@@ -947,8 +1028,7 @@ async function handleStop() {
947
1028
  // An obligation that cannot name its endpoint cannot be proven discharged.
948
1029
  if (!endpoint) return false;
949
1030
  try {
950
- const { probeProxyLiveness } = await import("../update/proxy-liveness-probe.mjs");
951
- return probeProxyLiveness(endpoint.port, endpoint.hostname) === "dead";
1031
+ return await probeEndpointLiveness(endpoint) === "dead";
952
1032
  } catch {
953
1033
  // A probe that could not run is not evidence of absence.
954
1034
  return false;
@@ -972,6 +1052,16 @@ async function handleStop() {
972
1052
  // service — the exact failure this flag prevents. A plain stop failure is different: we
973
1053
  // tried, so local teardown still proceeds.
974
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
+ };
975
1065
  // Deferring shared teardown to this process is an obligation, so record it on disk
976
1066
  // before asking for it (#3008). A parent that dies mid-stop would otherwise leave the
977
1067
  // client config routed at a proxy that is already gone, with nothing to find later.
@@ -1049,6 +1139,7 @@ async function handleStop() {
1049
1139
  };
1050
1140
  try {
1051
1141
  const serviceStop = stopServiceIfInstalledDetailed();
1142
+ record.service = serviceStop;
1052
1143
  stoppedService = serviceStop === "stopped" || serviceStop === "stopped-respawnable";
1053
1144
  schedulerCanRespawn = serviceStop === "stopped-respawnable";
1054
1145
  // No "won't respawn" claim here: a stopped Task Scheduler can still respawn through
@@ -1069,6 +1160,7 @@ async function handleStop() {
1069
1160
  console.error(" Run 'ocx service status' to see the query error, repair Task Scheduler access, then retry.");
1070
1161
  }
1071
1162
  } catch (err) {
1163
+ record.service = "error";
1072
1164
  if (isServiceOwnershipError(err)) {
1073
1165
  ownershipBlocked = true;
1074
1166
  stopFailed = true;
@@ -1092,8 +1184,10 @@ async function handleStop() {
1092
1184
  console.log(`āœ… Proxy (PID ${pid}) stopped.`);
1093
1185
  removePid(pid);
1094
1186
  removeRuntimePort(pid);
1187
+ record.proxy = "stopped";
1095
1188
  } catch (err) {
1096
1189
  stopFailed = true;
1190
+ record.proxy = err instanceof ProxyOwnershipRefusedError ? "ownership-refused" : "stop-failed";
1097
1191
  console.error(`āŒ Failed to stop proxy (PID ${pid}).`);
1098
1192
  // stopProxy throws with the reason — an ownership refusal (409) carries the
1099
1193
  // remediation ("run the stop from that home"). Swallowing it leaves the operator
@@ -1128,8 +1222,10 @@ async function handleStop() {
1128
1222
  { hostname: live.hostname ?? "127.0.0.1", port: live.port },
1129
1223
  );
1130
1224
  console.log(`āœ… Proxy (PID ${live.pid}) stopped.`);
1225
+ record.proxy = "stopped-orphan";
1131
1226
  } catch (err) {
1132
1227
  stopFailed = true;
1228
+ record.proxy = err instanceof ProxyOwnershipRefusedError ? "ownership-refused" : "stop-failed";
1133
1229
  console.error(`āŒ Failed to stop proxy (PID ${live.pid}).`);
1134
1230
  const detail = err instanceof Error ? err.message : String(err);
1135
1231
  if (detail) console.error(` ${detail}`);
@@ -1147,12 +1243,14 @@ async function handleStop() {
1147
1243
  // under a proxy that is still serving — the exact failure the deferral exists to
1148
1244
  // prevent, arrived at from the other direction.
1149
1245
  stopFailed = true;
1246
+ record.proxy = "unresolvable-pid";
1150
1247
  ownershipBlocked = true;
1151
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.`);
1152
1249
  console.error(" Skipping shared teardown: restoring client config while it serves would leave both pointing at each other.");
1153
1250
  console.error(" Stop it from the home that started it, or end the process manually, then rerun 'ocx stop'.");
1154
- } else if (!stoppedService) {
1155
- console.log("No running proxy found.");
1251
+ } else {
1252
+ record.proxy = "not-running";
1253
+ if (!stoppedService) console.log("No running proxy found.");
1156
1254
  }
1157
1255
  if (!stopFailed) {
1158
1256
  // `readPid() === null` means the snapshotted pid file was absent, invalid, dead, or
@@ -1175,6 +1273,7 @@ async function handleStop() {
1175
1273
  const survivor = await proxyStillLiveAfterStop({ canRespawn: true });
1176
1274
  if (survivor) {
1177
1275
  stopFailed = true;
1276
+ record.proxy = "respawned";
1178
1277
  console.error(`āŒ A proxy is still listening on port ${survivor.port} after the service stop; it is being respawned.`);
1179
1278
  console.error(" Skipping shared teardown: restoring client config while the proxy runs leaves both pointing at each other.");
1180
1279
  ownershipBlocked = true;
@@ -1210,6 +1309,7 @@ async function handleStop() {
1210
1309
  // No file, no nonce: nothing to quarantine and nothing to remove. The home itself
1211
1310
  // may be hiding an obligation, so block and ask for the directory to be fixed.
1212
1311
  inheritedBlocks = true;
1312
+ record.inheritedTeardownBlocks = true;
1213
1313
  stopFailed = true;
1214
1314
  console.error(`āŒ ${read.detail}, so this stop cannot tell whether a shared teardown is still owed.`);
1215
1315
  console.error(" Skipping shared teardown. Fix access to the opencodex home, then rerun 'ocx stop'.");
@@ -1218,6 +1318,7 @@ async function handleStop() {
1218
1318
  if (read.state === "invalid") {
1219
1319
  unreadable.push(read);
1220
1320
  inheritedBlocks = true;
1321
+ record.inheritedTeardownBlocks = true;
1221
1322
  stopFailed = true;
1222
1323
  console.error(`āŒ A pending-teardown receipt could not be read (${read.detail}).`);
1223
1324
  console.error(" It names no endpoint, so this stop cannot prove the proxy it belonged to is down.");
@@ -1229,6 +1330,7 @@ async function handleStop() {
1229
1330
  // proxy on an explicit --port can be respawned there while this address refuses,
1230
1331
  // so "dead" here proves nothing and must not authorize a restore.
1231
1332
  inheritedBlocks = true;
1333
+ record.inheritedTeardownBlocks = true;
1232
1334
  stopFailed = true;
1233
1335
  console.error("āŒ A shared teardown from an earlier stop is outstanding, but that stop could not record the address it was stopping.");
1234
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.`);
@@ -1240,12 +1342,14 @@ async function handleStop() {
1240
1342
  continue;
1241
1343
  }
1242
1344
  inheritedBlocks = true;
1345
+ record.inheritedTeardownBlocks = true;
1243
1346
  stopFailed = true;
1244
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.`);
1245
1348
  console.error(" Skipping shared teardown: restoring client config under a proxy that may still be running is what the deferral exists to prevent.");
1246
1349
  console.error(" The obligation is preserved; retry once the proxy is confirmed stopped.");
1247
1350
  }
1248
1351
  }
1352
+ if (nativeRestoreHandledByProxy) record.sharedTeardown = "performed-by-proxy";
1249
1353
  const restoreBlocked = ownershipBlocked || inheritedBlocks || nativeRestoreHandledByProxy;
1250
1354
  if (!restoreBlocked) {
1251
1355
  if (recoveredNonces.length > 0) {
@@ -1254,6 +1358,7 @@ async function handleStop() {
1254
1358
  console.log("ā†©ļø Finishing a shared teardown left unfinished by an earlier stop.");
1255
1359
  }
1256
1360
  const restore = await restoreSharedClientStateAfterStop();
1361
+ record.sharedTeardown = restore.historyDeferred ? "refused" : restore.other ? "failed" : "restored";
1257
1362
  if (restore.other) stopFailed = true;
1258
1363
  else if (restore.historyDeferred) historyDeferredNonces = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
1259
1364
  else if (restore.historyOnly) historyOnlyFailure = true;
@@ -1279,6 +1384,7 @@ async function handleStop() {
1279
1384
  // removal is surfaced rather than swallowed.
1280
1385
  if (!clearPendingTeardown(nonce)) {
1281
1386
  stopFailed = true;
1387
+ record.receiptClearFailed = true;
1282
1388
  console.error(`āŒ The shared teardown finished, but its receipt could not be removed: ${pendingTeardownPathFor(nonce)}`);
1283
1389
  console.error(" Remove it manually; otherwise every later stop and update will try to recover it again.");
1284
1390
  }
@@ -1323,18 +1429,23 @@ async function handleStop() {
1323
1429
  ? STOP_HISTORY_DEFERRED_EXIT_CODE
1324
1430
  : 1;
1325
1431
  }
1326
- 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 };
1327
1439
  }
1328
1440
 
1329
1441
  async function handleUninstall() {
1330
1442
  /** Definitive "nothing is answering" on the endpoint this home would serve. */
1331
1443
  const proxyEndpointProvenDown = async (): Promise<boolean> => {
1332
1444
  try {
1333
- const { probeProxyLiveness } = await import("../update/proxy-liveness-probe.mjs");
1334
1445
  // Every candidate, not just the preferred one: a stale runtime record pointing at a
1335
1446
  // closed port would otherwise "prove" a live proxy on the configured port is gone.
1336
1447
  const endpoints = endpointsToProve(readRuntimePort(), loadConfig());
1337
- return everyEndpointProvenDown(endpoints, e => probeProxyLiveness(e.port, e.hostname));
1448
+ return await everyEndpointProvenDownAsync(endpoints, probeEndpointLiveness);
1338
1449
  } catch {
1339
1450
  return false;
1340
1451
  }
@@ -1581,8 +1692,26 @@ async function handleStatus() {
1581
1692
  console.log(installed
1582
1693
  ? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
1583
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
+ }
1584
1703
  }
1585
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
+ }
1586
1715
  console.log(` Config: ${status.json.paths.config}${local}`);
1587
1716
  console.log(` PID file: ${status.json.paths.pid}${local}`);
1588
1717
  console.log(` Runtime: ${status.json.paths.runtime}${local}`);
@@ -1601,6 +1730,12 @@ async function handleStatus() {
1601
1730
  console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
1602
1731
  console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
1603
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 : ""}`));
1604
1739
  if (status.json.startup.routingKind === "native") {
1605
1740
  let retainedProviderTable = false;
1606
1741
  try {
@@ -1729,6 +1864,14 @@ async function handleReady(args: ReadyArgs): Promise<number> {
1729
1864
  return runReady(args);
1730
1865
  }
1731
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
+
1732
1875
  process.exit(await dispatchCommand(head, {
1733
1876
  args,
1734
1877
  command,
@@ -1749,6 +1892,7 @@ process.exit(await dispatchCommand(head, {
1749
1892
  },
1750
1893
  handleStart,
1751
1894
  handleStop,
1895
+ handleResolve,
1752
1896
  handleEnsure,
1753
1897
  handleTrayProxyStart,
1754
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
  }
@@ -41,7 +41,7 @@ import type {
41
41
  } from "../clients/config-export";
42
42
  import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
43
43
  import { commandInvocation } from "../lib/win-exec";
44
- import { configuredAdminToken } from "../lib/admin-secrets";
44
+ import { configuredAdminToken, opencodeCatalogToken } from "../lib/admin-secrets";
45
45
  import { localManagementOrigin } from "../lib/local-destinations";
46
46
  import { directLocalHttpFetch } from "../server/direct-local-http";
47
47
  import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
@@ -684,7 +684,7 @@ export async function cmdOpencode(args: string[]): Promise<number> {
684
684
  try {
685
685
  const managementToken = configuredAdminToken();
686
686
  if (!managementToken) throw new Error("No local admin token is available; check the running proxy's home.");
687
- proxyModels = await fetchOpencodeProxyModels(live, managementToken, {
687
+ proxyModels = await fetchOpencodeProxyModels(live, opencodeCatalogToken(managementToken), {
688
688
  managementOrigin: localManagementOrigin({ ...startupConfig, hostname: live.hostname }, live.port),
689
689
  });
690
690
  } catch (error) {
@@ -139,7 +139,7 @@ function handleList(args: string[]): void {
139
139
  // provider add
140
140
  // ---------------------------------------------------------------------------
141
141
 
142
- const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
142
+ const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--google-tool-schema-policy <compatible|reject-lossy>] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
143
143
 
144
144
  async function handleAdd(args: string[]): Promise<void> {
145
145
  const name = args[0];
@@ -164,6 +164,7 @@ async function handleAdd(args: string[]): Promise<void> {
164
164
  const adapter = consumeFlagValue(restArgs, "--adapter");
165
165
  const baseUrl = consumeFlagValue(restArgs, "--base-url");
166
166
  const defaultModel = consumeFlagValue(restArgs, "--default-model");
167
+ const googleToolSchemaPolicy = consumeFlagValue(restArgs, "--google-tool-schema-policy");
167
168
  const textOnly = consumeFlag(restArgs, "--text-only");
168
169
  const capabilityModel = consumeFlagValue(restArgs, "--model");
169
170
  rejectUnknownArgs(restArgs, ADD_USAGE);
@@ -228,6 +229,17 @@ async function handleAdd(args: string[]): Promise<void> {
228
229
  }
229
230
  provConfig.apiKeyTransport = apiKeyTransport;
230
231
  }
232
+ if (googleToolSchemaPolicy !== undefined) {
233
+ if (googleToolSchemaPolicy !== "compatible" && googleToolSchemaPolicy !== "reject-lossy") {
234
+ console.error('Error: --google-tool-schema-policy must be "compatible" or "reject-lossy".');
235
+ process.exit(1);
236
+ }
237
+ if (provConfig.adapter !== "google") {
238
+ console.error("Error: --google-tool-schema-policy requires the google adapter.");
239
+ process.exit(1);
240
+ }
241
+ provConfig.googleToolSchemaPolicy = googleToolSchemaPolicy;
242
+ }
231
243
 
232
244
  const existingProvider = config.providers[name];
233
245
  if (existingProvider?.modelCapabilities !== undefined && provConfig.modelCapabilities === undefined) {