@iislee/opencodex 2.11.0 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
package/src/cli/index.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from "node:child_process";
3
- import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
3
+ import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
4
4
  import { stripGrokConfig } from "../grok/inject";
5
- import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
5
+ import {
6
+ describeHistoryJobFailure,
7
+ resolveCodexHistoryJobTarget,
8
+ runCodexHistoryJob,
9
+ } from "../codex/history-job";
6
10
  import { reconcileJournal } from "../codex/journal";
7
11
  import {
8
12
  codexAutoStartEnabled,
@@ -20,51 +24,79 @@ import {
20
24
  writeRuntimePort,
21
25
  } from "../config";
22
26
  import { collectStatus } from "./status";
23
- import { dispatchInternalCliCommand, type InternalCliCommand } from "./internal-dispatch";
24
- import { runTrayProxyRestart, runTrayProxyStart } from "./tray-proxy";
27
+
28
+ import {
29
+ discoverStableProxyForRestart,
30
+ isProxyReplacement,
31
+ runProxyRestart,
32
+ runTrayProxyStart,
33
+ type ProxyRestartLive,
34
+ type ProxyRestartResult,
35
+ } from "./tray-proxy";
36
+ import { requestBoundSystemRestart } from "./system-restart-client";
25
37
  import { installCrashGuards } from "../lib/crash-guard";
26
- import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
38
+ import { dispatchCommand } from "./dispatch";
27
39
  import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
28
40
  import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
29
- import { stopProxy } from "../lib/process-control";
41
+ import { createReadinessGate } from "../server/readiness";
42
+ import { runReady, type ReadyArgs } from "./ready";
43
+ import { runCli } from "./root";
44
+ import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
30
45
  import { loadServiceTokenFromFile } from "../lib/service-secrets";
31
46
  import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
32
47
  import { startupHealthSummary } from "../codex/autostart-health";
33
48
  import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
34
- import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
49
+ import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
35
50
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
36
- import { installShellHook, uninstallShellHook } from "../server/system-env";
37
51
  import { startTokenGuardian } from "../oauth/token-guardian";
38
52
  import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
39
- import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
40
53
  import { maybeShowStarPrompt } from "./star-prompt";
41
54
  import { scheduleCatalogPrewarm } from "./catalog-prewarm";
42
55
  import { maybeShowUpdatePrompt } from "../update/notify";
43
56
  import { syncModelsToCodex } from "../codex/sync";
44
- import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
45
- import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
46
- import { removeOwnedConfigState } from "../lib/config-ownership";
47
-
48
- const args = process.argv.slice(2);
49
- const command = args[0];
50
-
51
- if (command === "--version" || command === "-v" || command === "version") {
52
- printVersion();
53
- process.exit(0);
54
- }
57
+ import {
58
+ shouldSyncGrokOnStart,
59
+ syncCodexOnStartIfEnabled,
60
+ } from "../codex/desired-state";
61
+ import {
62
+ reconcileClientStartupBeforeReady,
63
+ syncClaudeAgentDefsAtProxyStartup,
64
+ } from "./claude-agent-startup-sync";
65
+ import {
66
+ grokSyncFailureMessage,
67
+ reconcileEnsureDesiredIntegrations,
68
+ } from "./ensure-desired-integrations";
55
69
 
56
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
57
- if (command === "help" && args[1]) printSubcommandUsage(args[1]);
58
- else printUsage();
59
- process.exit(0);
70
+ /**
71
+ * A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
72
+ * `claude-env.sh` from every new interactive shell, pointing at a proxy or a CLI that may no
73
+ * longer exist. `reconcileShellHook` already reports `state: "failed"`, but both call sites
74
+ * discarded it, so the one outcome the user has to act on was the one they never saw.
75
+ */
76
+ function reportShellHookFailure(result: { state: "installed" | "absent" | "failed"; reason?: string }): void {
77
+ if (result.state !== "failed") return;
78
+ console.warn(` Claude shell hook not reconciled${result.reason ? `: ${result.reason}` : ""}`);
79
+ console.warn(" Check ~/.zshrc for the '# opencodex claude-env hook' block.");
60
80
  }
61
81
 
62
- if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
63
- printSubcommandUsage(command);
64
- process.exit(0);
65
- }
66
82
 
67
- maybeAutoRestoreCodexShim(command, args);
83
+ import { removeOwnedConfigState } from "../lib/config-ownership";
84
+ import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
85
+ import { selfLaunchArgv } from "../lib/self-launch-argv";
86
+ import { initializeNodeLauncherContext } from "./launcher-context";
87
+ import { createLocalAttestationSecret } from "../lib/local-management-attestation";
88
+ import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
89
+
90
+ initializeNodeLauncherContext();
91
+
92
+ // Head: version/help early exits, `ready` pre-parse (exit 64 before any
93
+ // preflight), and the bounded Codex-shim auto-restore preflight live in
94
+ // src/cli/root.ts (Phase 1 of the CLI deepening). runCli exits for
95
+ // version/help and returns the dispatchable head otherwise; the switch below
96
+ // owns command dispatch.
97
+ const head = await runCli(process.argv.slice(2));
98
+ const args = head.args;
99
+ const command = head.command;
68
100
 
69
101
  function parsePortOption(): number | undefined {
70
102
  if (args.length === 1) return undefined;
@@ -95,34 +127,31 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
95
127
  return null;
96
128
  }
97
129
 
98
- /**
99
- * A Grok fence sync that throws is best-effort by design — it must never block startup.
100
- * Reporting nothing, however, is what lets a STALE fence survive: `~/.grok/config.toml`
101
- * keeps naming whatever port the last successful sync wrote, and once that listener is
102
- * gone every grok turn retries against a refused connection while our own log stays
103
- * silent (2026-07-27 field report: 8 entries pinned to a dead 127.0.0.1:4179).
104
- * So say what failed and name the single command that repairs it.
105
- */
106
- function grokSyncFailureMessage(err: unknown): string {
107
- const detail = err instanceof Error ? err.message : String(err);
108
- return `Grok Build config sync failed: ${detail}. `
109
- + "~/.grok/config.toml may still point at a previous proxy port — "
110
- + "run 'ocx ensure' (or apply from the dashboard's Grok page) to repoint it.";
111
- }
112
-
113
130
  /** Argv for detached `start`, optionally hard-pinning the listen port. */
114
131
  function startArgv(port?: number): string[] {
115
- const args = [process.argv[1], "start"];
132
+ const args = ["start"];
116
133
  if (typeof port === "number" && Number.isFinite(port) && port > 0 && port <= 65535) {
117
134
  args.push("--port", String(Math.trunc(port)));
118
135
  }
119
- return args;
136
+ return selfLaunchArgv(args);
120
137
  }
121
138
 
122
139
  async function chooseListenPort(requestedPort?: number): Promise<number> {
123
140
  const config = loadConfig();
124
141
  const preferred = requestedPort ?? config.port ?? 10100;
125
142
  const hardPin = requestedPort !== undefined && requestedPort > 0;
143
+ const reservedLoopbackPort = config.unauthenticatedLoopbackListener?.enabled
144
+ ? config.unauthenticatedLoopbackListener.port
145
+ : undefined;
146
+ // Before the reclaim path, not after (#1102). Asking for the port the loopback listener is
147
+ // configured to bind is a configuration mistake, and reclaim would spend up to 60 seconds
148
+ // waiting for a socket to free before reporting "port is busy" — the wrong diagnosis for a
149
+ // collision the config can state outright.
150
+ if (reservedLoopbackPort !== undefined && preferred === reservedLoopbackPort) {
151
+ throw new Error(
152
+ `Port ${preferred} is reserved for unauthenticatedLoopbackListener; choose a different proxy port.`,
153
+ );
154
+ }
126
155
  // Soft start: brief prefer-retry then ephemeral hop.
127
156
  // Explicit `--port` (service wrappers / update restart): wait for the pinned port
128
157
  // to free without killing any listener (healthy ocx / foreign). Never hop.
@@ -146,6 +175,11 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
146
175
  preferRetryMs: hardPin ? 5_000 : 750,
147
176
  preferRetryIntervalMs: 50,
148
177
  allowEphemeralFallback: !hardPin,
178
+ // Never hand the public listener the port the loopback listener is configured to
179
+ // bind (#1102). Without this, `--port <loopback port>` binds the public listener
180
+ // first and the loopback bind then fails, rolling back a startup that was only
181
+ // ever a config collision.
182
+ ...(reservedLoopbackPort !== undefined ? { reservedPort: reservedLoopbackPort } : {}),
149
183
  });
150
184
  if (preferred > 0 && selected !== preferred) {
151
185
  console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
@@ -165,6 +199,22 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
165
199
  }
166
200
  }
167
201
 
202
+ async function findProxyOwnerBeforeJournalRecovery(
203
+ options: { probeConfiguredPort?: boolean } = {},
204
+ ): Promise<{ live: LiveProxy | null; pidSnapshot: number | null }> {
205
+ const pidSnapshot = readPidFileValue();
206
+ const hasRuntimeOwner = readRuntimePort() !== null;
207
+ const shouldProbe = pidSnapshot !== null || hasRuntimeOwner || options.probeConfiguredPort === true;
208
+ const live = shouldProbe ? await findLiveProxy() : null;
209
+ if (live) return { live, pidSnapshot };
210
+
211
+ // The probe established that the snapshotted owner is stale. Compare before
212
+ // deleting so a concurrent start that rewrote the PID file keeps its state.
213
+ removePidIfValueIs(pidSnapshot);
214
+ if (!currentExternalCodexModelProvider()) reconcileJournal();
215
+ return { live: null, pidSnapshot };
216
+ }
217
+
168
218
  async function handleStart(options: { block?: boolean } = {}) {
169
219
  // Native (WinSW) service mode has no batch wrapper to read the service token file
170
220
  // into the environment, so the app loads it here before the server binds. The server
@@ -172,15 +222,21 @@ async function handleStart(options: { block?: boolean } = {}) {
172
222
  const serviceToken = loadServiceTokenFromFile(process.env);
173
223
  if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
174
224
  const requestedPort = parsePortOption();
175
- if (!currentExternalCodexModelProvider()) reconcileJournal();
176
- const existingPid = readPid();
177
- if (existingPid) {
178
- const live = await findLiveProxy();
179
- if (live) {
180
- console.error(`⚠️ Proxy already running (PID ${live.pid ?? existingPid}, port ${live.port}). Use 'ocx stop' first.`);
181
- process.exit(1);
182
- }
183
- removePid(existingPid);
225
+ const owner = await findProxyOwnerBeforeJournalRecovery();
226
+ if (owner.live) {
227
+ // Service-wrapper context (opencodex-service.cmd `:loop`): a healthy proxy from
228
+ // ANY source means the requested port is already served. Exit 0 so the wrapper's
229
+ // `if %ERRORLEVEL% NEQ 0` retry loop terminates instead of respawning every 5s
230
+ // against a listener it can never claim (observed as an endless
231
+ // "Proxy already running" service.log loop).
232
+ // Only the exact "1" sentinel takes this path — the same check syncCleanup
233
+ // uses — so an env value like "0" or "false" cannot bypass the conflict error.
234
+ if (process.env.OCX_SERVICE === "1") {
235
+ console.log(`Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}); service wrapper staying out of the way.`);
236
+ process.exit(0);
237
+ }
238
+ console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
239
+ process.exit(1);
184
240
  }
185
241
 
186
242
  // Interactive-only update prompt. Must run BEFORE we bind a port / write a
@@ -192,10 +248,16 @@ async function handleStart(options: { block?: boolean } = {}) {
192
248
  // between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
193
249
  // the same port only (never hop — that was the remaining PR #152 gap).
194
250
  let port = await chooseListenPort(requestedPort);
251
+ // One private readiness gate for this startServer invocation, captured by the
252
+ // listener's closure. handleStart owns it and transitions it after the
253
+ // post-startup sync settles. A second startServer in the same process would
254
+ // get its own gate and could never reset/mutate this one.
255
+ const readinessGate = createReadinessGate();
195
256
  let server: ReturnType<typeof startServer>;
257
+ const localAttestationSecret = createLocalAttestationSecret();
196
258
  for (let attempt = 0; ; attempt++) {
197
259
  try {
198
- server = startServer(port);
260
+ server = startServer(port, { localAttestationSecret, readinessGate });
199
261
  // Prewarm the live provider model cache as soon as the port is bound so the
200
262
  // first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
201
263
  // instead of racing duplicate upstream /models fetches.
@@ -223,7 +285,7 @@ async function handleStart(options: { block?: boolean } = {}) {
223
285
  writePid(process.pid);
224
286
 
225
287
  const config = loadConfig();
226
- writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
288
+ writeRuntimePort({ pid: process.pid, port, hostname: config.hostname, attestationSecret: localAttestationSecret });
227
289
  // No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
228
290
  // is about to transform; snapshotting earlier only captured a baseline that could
229
291
  // already be stale by the time injection ran (#477).
@@ -251,7 +313,8 @@ async function handleStart(options: { block?: boolean } = {}) {
251
313
  }
252
314
  removePid(process.pid);
253
315
  removeRuntimePort(process.pid);
254
- if (!recycling && !process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
316
+ const preserveRouting = process.env.OCX_SERVICE === "1";
317
+ if (!recycling && !preserveRouting && !currentExternalCodexModelProvider()) {
255
318
  try {
256
319
  const restored = restoreNativeCodex();
257
320
  if (!restored.success) {
@@ -267,7 +330,7 @@ async function handleStart(options: { block?: boolean } = {}) {
267
330
  // Grok fence is shared state we must not remove — that service keeps running and would be
268
331
  // left pointing nowhere. This guard also covers signal-driven exits, which is the path that
269
332
  // would otherwise bypass handleStop's gate entirely.
270
- if (!recycling && !process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
333
+ if (!recycling && !preserveRouting && serviceEnvironmentOwnedHere()) {
271
334
  try { stripGrokConfig(); } catch { /* best-effort restore */ }
272
335
  }
273
336
  return cleanupSucceeded;
@@ -310,12 +373,32 @@ async function handleStart(options: { block?: boolean } = {}) {
310
373
 
311
374
  // System-wide env injection AFTER signal handlers are registered (crash safety:
312
375
  // syncCleanup reverts even if injection itself or subsequent startup steps fail).
313
- await injectSystemEnv(port, config).catch(() => {});
314
- // Auto-install .zshrc hook (idempotent skips if already present).
315
- installShellHook();
316
-
376
+ const systemEnv = await injectSystemEnv(port, config).catch(() => ({ injected: false }));
377
+ // The hook is useful only for an installed Claude Code CLI. Reconcile instead of
378
+ // appending unconditionally so stale OpenCodex-owned hooks are removed as well.
379
+ reportShellHookFailure(reconcileShellHook(systemEnv.injected));
317
380
  await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
318
- await syncModelsToCodex(port).catch(() => {});
381
+ // Codex sync owns the ready/failed verdict, but its successful transition is
382
+ // deferred until the best-effort Claude roster reconciliation settles. This
383
+ // keeps /readyz closed across both startup writes without making an optional
384
+ // Claude integration failure prevent the proxy from starting.
385
+ const startupSync = await reconcileClientStartupBeforeReady(
386
+ readinessGate,
387
+ gate => syncCodexOnStartIfEnabled(port, config, undefined, gate),
388
+ () => systemEnv.injected
389
+ ? Promise.resolve(null)
390
+ : syncClaudeAgentDefsAtProxyStartup(config, port),
391
+ );
392
+ if (!startupSync.ran) console.log(" Codex integration OFF; startup left Codex native.");
393
+ // #1046: one warning per startup, after BOTH writes. The server's cache
394
+ // invalidation happens first and the catalog sync second, so the mtime is only
395
+ // final here — and neither write site warns on its own, or a boot that hits
396
+ // both would warn twice.
397
+ const { consumeStartupCacheInvalidationWrite } = await import("../server");
398
+ if (consumeStartupCacheInvalidationWrite() || startupSync.catalogWritten || startupSync.cacheSynced) {
399
+ const { warnIfStaleCodexAppServersAfterStartupWrite } = await import("../codex/app-server-processes");
400
+ warnIfStaleCodexAppServersAfterStartupWrite({ log: console });
401
+ }
319
402
  if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
320
403
  historyGuardian = startHistoryMigrationGuardian();
321
404
  }
@@ -335,7 +418,10 @@ async function handleStart(options: { block?: boolean } = {}) {
335
418
  // absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
336
419
  // Deliberately a SIBLING of the Desktop-3P block above: nesting it there meant a catalog
337
420
  // failure skipped the fence entirely, even though syncGrokConfig handles that case itself.
338
- try {
421
+ //
422
+ // Gated on the persisted switch: without this, turning Grok off lasted exactly
423
+ // one restart, because the toggle removed the fence and start wrote it back.
424
+ if (shouldSyncGrokOnStart(config)) try {
339
425
  const { syncGrokConfig } = await import("../grok/sync");
340
426
  const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
341
427
  if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
@@ -354,30 +440,46 @@ async function handleStart(options: { block?: boolean } = {}) {
354
440
  }
355
441
  }
356
442
 
357
- async function handleEnsure() {
358
- if (!currentExternalCodexModelProvider()) reconcileJournal();
443
+ function detachedStartEnvironment(): NodeJS.ProcessEnv {
444
+ const env: NodeJS.ProcessEnv = { ...process.env };
445
+ // Only a real service wrapper may claim supervision. A detached ensure/tray child
446
+ // is an ordinary owner: while live it maintains routing, and on exit it restores it.
447
+ delete env.OCX_SERVICE;
448
+ return withProcessRuntimeProvenance(env);
449
+ }
450
+
451
+ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Promise<boolean> {
452
+ const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
359
453
  const config = loadConfig();
360
454
  if (!codexAutoStartEnabled(config)) {
361
455
  console.log("Codex autostart is disabled.");
362
- return;
456
+ return false;
363
457
  }
364
- const live = await findLiveProxy();
365
- if (live) {
366
- await syncModelsToCodex(live.port).catch(e => {
458
+ const live = owner.live;
459
+ if (live) {
460
+ if (options.existingIsSuccess === false) {
461
+ console.error("Proxy appeared while restart was confirming absence; no start was attempted.");
462
+ return false;
463
+ }
464
+ const synced = await syncModelsToCodex(live.port).catch(e => {
367
465
  console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
466
+ return null;
368
467
  });
468
+ if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
369
469
  // Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
370
- await injectSystemEnv(live.port, config).catch(() => {});
470
+ const systemEnv = await injectSystemEnv(live.port, config).catch(() => ({ injected: false }));
471
+ reportShellHookFailure(reconcileShellHook(systemEnv.injected));
472
+ if (!systemEnv.injected) await syncClaudeAgentDefsAtProxyStartup(config, live.port);
371
473
  // Refresh the Grok Build fence too (same contract as start). live.hostname is the
372
474
  // hostname the running proxy actually bound — config.hostname may have drifted.
373
- try {
374
- const { syncGrokConfig } = await import("../grok/sync");
375
- const g = await syncGrokConfig(live.port, config, live.hostname ? { hostname: live.hostname } : {});
376
- if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
377
- else if (!g.ok) console.error(`⚠️ ${g.message}`);
378
- } catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
475
+ // The reconciler re-reads immediately before each client-file mutation; only
476
+ // the live proxy's observed bind host is safe to carry across this boundary.
477
+ await reconcileEnsureDesiredIntegrations(
478
+ live.port,
479
+ { kind: "live", hostname: live.hostname },
480
+ );
379
481
  console.log(`✅ Proxy running on port ${live.port}`);
380
- return;
482
+ return true;
381
483
  }
382
484
 
383
485
  const pinPort = config.port ?? 10100;
@@ -385,36 +487,42 @@ async function handleEnsure() {
385
487
  detached: true,
386
488
  stdio: "ignore",
387
489
  windowsHide: true,
388
- env: { ...process.env, OCX_SERVICE: "1" },
490
+ env: detachedStartEnvironment(),
389
491
  });
390
492
  child.unref();
391
493
 
392
494
  const port = (await waitForProxy())?.port;
393
495
  if (!port) {
394
496
  console.error("❌ Proxy did not become healthy after starting.");
395
- process.exit(1);
396
- }
397
- // Deterministic fence guarantee: the spawned child injects late in its own startup, but
398
- // this parent returns as soon as /healthz responds inject here too (idempotent block
399
- // replace) so `ocx ensure` never returns without the Grok fence in place.
400
- try {
401
- const { syncGrokConfig } = await import("../grok/sync");
402
- const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
403
- if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
404
- else if (!g.ok) console.error(`⚠️ ${g.message}`);
405
- } catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
497
+ process.exitCode = 1;
498
+ return false;
499
+ }
500
+ // Deterministic fence guarantee when the durable switch is ON: the spawned child
501
+ // injects late in its own startup, but this parent returns as soon as /healthz
502
+ // responds — align here too so `ocx ensure` never returns with a stale ON/OFF mismatch.
503
+ // Persisted state is loaded inside each mutation after waitForProxy, so a
504
+ // toggle while the child starts wins over the pre-spawn snapshot.
505
+ await reconcileEnsureDesiredIntegrations(port, { kind: "spawned" });
406
506
  // Always sync the LIVE port: after a fallback-port start, config.port still names the
407
507
  // busy preferred port — syncing that would point Codex at a dead listener.
408
- await syncModelsToCodex(port).catch(e => {
508
+ const synced = await syncModelsToCodex(port).catch(e => {
409
509
  console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
510
+ return null;
410
511
  });
512
+ if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
513
+ // The child opens /healthz before its best-effort roster reconcile. Await the same idempotent
514
+ // operation in the parent so `ocx ensure` cannot report success while stale ocx-*.md files are
515
+ // still observable. Always use the live port, including fallback-port starts.
516
+ await syncClaudeAgentDefsAtProxyStartup(config, port);
411
517
  console.log(`✅ Proxy running on port ${port}`);
518
+ return true;
412
519
  }
413
520
 
414
521
  /** Fixed tray action: start the proxy without depending on codexAutoStart. */
415
- async function handleTrayProxyStart(): Promise<void> {
522
+ async function handleTrayProxyStart(existingIsSuccess = true): Promise<boolean> {
416
523
  const ok = await runTrayProxyStart({
417
524
  findLive: findLiveProxy,
525
+ existingIsSuccess,
418
526
  diagnoseService: () => {
419
527
  const service = diagnoseService();
420
528
  return { installed: service.installed, startable: serviceStartableFromTray(service), summary: service.summary };
@@ -427,29 +535,117 @@ async function handleTrayProxyStart(): Promise<void> {
427
535
  detached: true,
428
536
  stdio: "ignore",
429
537
  windowsHide: true,
430
- env: { ...process.env, OCX_SERVICE: "1" },
538
+ env: detachedStartEnvironment(),
431
539
  });
432
540
  child.unref();
433
541
  },
434
- waitForProxy,
542
+ // serviceCommand("start") already spends up to 20s confirming the supervised
543
+ // child. Slow Windows hosts can still be publishing native-main state after that
544
+ // first window, so keep one shared follow-up budget instead of returning a false
545
+ // failure while Task Scheduler is still starting the approved child.
546
+ waitForProxy: () => waitForProxy(40_000),
435
547
  info: message => console.log(message),
436
548
  error: message => console.error(message),
437
549
  });
438
- if (!ok) process.exitCode = 1;
550
+ // serviceCommand("start") can set exitCode=1 after its own 20s probe, while
551
+ // the coordinator's bounded follow-up observes the same service become live.
552
+ // The final observed state, not the earlier probe, owns this command result.
553
+ process.exitCode = ok ? 0 : 1;
554
+ return ok;
439
555
  }
440
556
 
441
- async function handleTrayProxyRestart(): Promise<void> {
442
- const ok = await runTrayProxyRestart({
443
- stop: async () => {
444
- await handleStop();
445
- return !process.exitCode || process.exitCode === 0;
446
- },
447
- start: async () => {
448
- await handleTrayProxyStart();
449
- return !process.exitCode || process.exitCode === 0;
450
- },
557
+ const PROXY_RESTART_OBSERVE_MS = MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIMEOUT_MS + 15_000;
558
+
559
+ async function waitForProxyReplacement(
560
+ previous: ProxyRestartLive,
561
+ deadlineAt: number,
562
+ ): Promise<ProxyRestartLive | null> {
563
+ while (Date.now() < deadlineAt) {
564
+ const live = await findLiveProxy({ deadlineAt });
565
+ if (Date.now() >= deadlineAt) return null;
566
+ // Modern /healthz publishes a PID. Require a different, identity-verified process;
567
+ // merely seeing the old port online again is not proof that restart completed.
568
+ if (isProxyReplacement(previous, live)) {
569
+ return live;
570
+ }
571
+ const remainingMs = deadlineAt - Date.now();
572
+ if (remainingMs > 0) await Bun.sleep(Math.min(250, remainingMs));
573
+ }
574
+ return null;
575
+ }
576
+
577
+ function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
578
+ if (result.phase === "identity") {
579
+ console.error("❌ Refusing to restart because the running proxy identity could not be attested.");
580
+ } else if (result.phase === "request") {
581
+ const code = result.error instanceof Error ? result.error.message : "";
582
+ if (code === "restart_capability_unsupported") {
583
+ console.error("❌ The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
584
+ console.error(" After confirming this home owns the proxy, run `ocx stop` and then `ocx start` once.");
585
+ } else {
586
+ console.error("❌ Proxy restart request could not be confirmed; no fallback stop/start was attempted.");
587
+ }
588
+ } else if (result.phase === "replacement") {
589
+ console.error("❌ Proxy restart was accepted, but no identity-verified replacement became healthy in time.");
590
+ } else {
591
+ console.error("❌ Proxy was not running and the fallback start did not become healthy.");
592
+ }
593
+ }
594
+
595
+ async function handleProxyRestart(
596
+ startWhenStopped: () => Promise<boolean | "skipped">,
597
+ ): Promise<boolean> {
598
+ const deadlineAt = Date.now() + PROXY_RESTART_OBSERVE_MS;
599
+ const result = await runProxyRestart({
600
+ findLive: () => discoverStableProxyForRestart({
601
+ findLive: () => findLiveProxy({ deadlineAt, attempts: 2 }),
602
+ expired: () => Date.now() >= deadlineAt,
603
+ }),
604
+ startWhenStopped,
605
+ requestInPlaceRestart: previous => requestBoundSystemRestart(previous, deadlineAt),
606
+ waitForReplacement: previous => waitForProxyReplacement(previous, deadlineAt),
451
607
  });
452
- if (!ok) process.exitCode = 1;
608
+ if (!result.ok) reportRestartFailure(result);
609
+ process.exitCode = result.ok ? 0 : 1;
610
+ return result.ok;
611
+ }
612
+
613
+ async function handleTrayProxyRestart(): Promise<void> {
614
+ await handleProxyRestart(() => handleTrayProxyStart(false));
615
+ }
616
+
617
+ async function handleRestartStartWhenStopped(): Promise<boolean | "skipped"> {
618
+ if (!codexAutoStartEnabled(loadConfig())) {
619
+ console.log("Codex autostart is disabled; no proxy was started.");
620
+ return "skipped";
621
+ }
622
+ return handleEnsure({ existingIsSuccess: false });
623
+ }
624
+
625
+ async function restoreSharedClientStateAfterStop(): Promise<boolean> {
626
+ let restored = true;
627
+ try {
628
+ const result = await restoreNativeCodexAsync();
629
+ if (result.success) console.log(`↩️ ${result.message}`);
630
+ else {
631
+ restored = false;
632
+ console.error(`⚠️ ${result.message}`);
633
+ }
634
+ } catch (error) {
635
+ restored = false;
636
+ console.error(`⚠️ Native Codex restore failed: ${error instanceof Error ? error.message : String(error)}`);
637
+ }
638
+
639
+ // A refused or thrown Grok strip is actionable because it would point Grok at a dead proxy.
640
+ try {
641
+ const grok = stripGrokConfig();
642
+ if (grok.changed) console.log(`↩️ ${grok.message}`);
643
+ else if (!grok.ok) { restored = false; console.error(`⚠️ ${grok.message}`); }
644
+ } catch (error) {
645
+ restored = false;
646
+ console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
647
+ }
648
+ return restored;
453
649
  }
454
650
 
455
651
  async function handleStop() {
@@ -493,6 +689,10 @@ async function handleStop() {
493
689
  // exact teardown the refusal exists to prevent.
494
690
  const detail = err instanceof Error ? err.message : String(err);
495
691
  if (detail) console.error(` ${detail}`);
692
+ if (err instanceof ProxyOwnershipRefusedError) {
693
+ ownershipBlocked = true;
694
+ console.error(" Skipping shared teardown (native Codex restore, Grok config): the foreign proxy is still running.");
695
+ }
496
696
  }
497
697
  } else {
498
698
  // Snapshot the stale on-disk state BEFORE the async probe: a concurrent `ocx start`
@@ -511,6 +711,10 @@ async function handleStop() {
511
711
  console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
512
712
  const detail = err instanceof Error ? err.message : String(err);
513
713
  if (detail) console.error(` ${detail}`);
714
+ if (err instanceof ProxyOwnershipRefusedError) {
715
+ ownershipBlocked = true;
716
+ console.error(" Skipping shared teardown (native Codex restore, Grok config): the foreign proxy is still running.");
717
+ }
514
718
  }
515
719
  } else if (!stoppedService) {
516
720
  console.log("No running proxy found.");
@@ -523,26 +727,11 @@ async function handleStop() {
523
727
  removeRuntimePortIfPidIs(staleRuntimePid);
524
728
  }
525
729
  }
526
- if (!ownershipBlocked) {
527
- const r = restoreNativeCodex();
528
- if (r.success) console.log(`↩️ ${r.message}`);
529
- else {
530
- stopFailed = true;
531
- console.error(`⚠️ ${r.message}`);
532
- }
533
- }
534
- // revertSystemEnv is NOT gated: it carries its own ownership check and concerns launchctl
535
- // user env, not CODEX_HOME. Safety net for when the daemon's syncCleanup didn't run (SIGKILL).
730
+ // Environment ownership is independent from service ownership. Always roll back
731
+ // current-home variables; the helper refuses foreign markers on its own.
536
732
  try { revertSystemEnv(); } catch { /* best-effort */ }
537
733
  if (!ownershipBlocked) {
538
- // Same safety net for the Grok Build managed block (marker-owned, idempotent).
539
- try {
540
- const g = stripGrokConfig();
541
- if (g.changed) console.log(`↩️ ${g.message}`);
542
- // A refused strip (e.g. orphaned marker) leaves the fence pointing at a dead proxy —
543
- // reporting success there hides a broken end state.
544
- else if (!g.ok) { stopFailed = true; console.error(`⚠️ ${g.message}`); }
545
- } catch { /* best-effort */ }
734
+ if (!await restoreSharedClientStateAfterStop()) stopFailed = true;
546
735
  }
547
736
  // Set the code rather than exiting inline: `restart` and the tray coordinator call this
548
737
  // function and need it to RETURN so they can decide what to do next.
@@ -586,8 +775,8 @@ async function handleUninstall() {
586
775
  });
587
776
  }
588
777
 
589
- await runStep("native Codex restored", () => {
590
- const r = restoreNativeCodex();
778
+ await runStep("native Codex restored", async () => {
779
+ const r = await restoreNativeCodexAsync();
591
780
  if (!r.success) throw new Error(r.message);
592
781
  });
593
782
 
@@ -634,7 +823,7 @@ async function handleUninstall() {
634
823
  console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
635
824
  process.exit(1);
636
825
  }
637
- console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @iislee/opencodex");
826
+ console.log("\n✅ opencodex local state removed. Remove the package with: npm uninstall -g @bitkyc08/opencodex");
638
827
  }
639
828
 
640
829
  async function handleStatus() {
@@ -659,7 +848,14 @@ async function handleStatus() {
659
848
  console.log(` Health: ${status.healthLabel}`);
660
849
  if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
661
850
  console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
662
- console.log(" Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
851
+ // The service summary a few lines below already tells a registered-but-not-serving
852
+ // user to repair. Printing "install the persistent service" unconditionally
853
+ // contradicted it in the same report, and install re-registers: UAC on Windows and a
854
+ // possible WinSW-to-scheduler switch for someone who already has a service.
855
+ const installed = status.json.startup.serviceInstalled && !status.json.startup.serviceConflict;
856
+ console.log(installed
857
+ ? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
858
+ : " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
663
859
  }
664
860
  console.log(` Dashboard: ${status.json.dashboard.url}`);
665
861
  console.log(` Config: ${status.json.paths.config}`);
@@ -707,407 +903,70 @@ async function handleStatus() {
707
903
  }
708
904
  }
709
905
 
710
- function handleRecoverHistory() {
906
+ async function handleRecoverHistory() {
711
907
  if (args[1] !== "--legacy-openai") {
712
908
  console.error("Usage: ocx recover-history --legacy-openai");
713
909
  console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
714
910
  process.exit(1);
715
911
  }
716
- const r = restoreLegacyOpenaiHistory();
912
+ // Manifest-independent legacy ejection, serialized like every other history
913
+ // mutation. It is a separate operation from generic restore precisely because
914
+ // it must not read, consume or replace the backup manifest.
915
+ const outcome = await runCodexHistoryJob({
916
+ ...resolveCodexHistoryJobTarget(),
917
+ operation: "recover-legacy-openai",
918
+ });
919
+ const r = outcome.kind === "converged"
920
+ ? { rows: outcome.rows, files: outcome.files, failed: undefined }
921
+ : { rows: 0, files: 0, failed: true as const };
717
922
  if (r.failed) {
718
923
  console.error(
719
- "⚠️ Recovery SKIPPED: the Codex history DB is locked (Codex app/IDE open?). Close it and rerun this command.",
924
+ `⚠️ Recovery SKIPPED: ${describeHistoryJobFailure(outcome, "recover-legacy")}`,
720
925
  );
721
926
  process.exit(1);
722
927
  }
723
928
  console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
724
929
  }
725
930
 
726
- switch (command) {
727
- case "init":
728
- case "setup": {
729
- const { runInit } = await import("./init");
730
- await runInit();
731
- break;
732
- }
733
- case "start":
734
- await handleStart();
735
- break;
736
- case "stop": {
737
- // Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
738
- // re-start the proxy immediately, so warning there would contradict the next line.
739
- if (await handleStop()) {
740
- console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').");
741
- }
742
- break;
743
- }
744
- case "restore":
745
- case "eject": {
746
- if (args[1] === "back") {
747
- // Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
748
- // lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
749
- // this only re-runs the same inject (config + catalog + history) `ocx start` does.
750
- const live = await findLiveProxy();
751
- if (!live) {
752
- console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
753
- process.exit(1);
754
- }
755
- const synced = await syncModelsToCodex(live.port);
756
- if (!synced.ok) {
757
- process.exitCode = 1;
758
- console.error("Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.");
759
- break;
760
- }
761
- const target = collectOrcaCodexHomeDiagnostic();
762
- console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
763
- break;
764
- }
765
- let r: { success: boolean; message: string };
766
- try {
767
- r = restoreNativeCodex();
768
- } catch (err) {
769
- r = { success: false, message: err instanceof Error ? err.message : String(err) };
770
- }
771
- if (r.success) console.log(`✅ ${r.message}`);
772
- else {
773
- console.error(`⚠️ ${r.message}`);
774
- process.exitCode = 1;
775
- }
776
- try {
777
- const g = stripGrokConfig();
778
- if (g.changed) console.log(`✅ ${g.message}`);
779
- else if (!g.ok) {
780
- console.error(`⚠️ ${g.message}`);
781
- process.exitCode = 1;
782
- }
783
- } catch { /* best-effort */ }
784
- if (r.success) {
785
- console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
786
- } else {
787
- console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
788
- }
789
- break;
790
- }
791
- case "recover-history":
792
- handleRecoverHistory();
793
- break;
794
- case "uninstall":
795
- case "remove":
796
- await handleUninstall();
797
- break;
798
- case "status":
799
- await handleStatus();
800
- break;
801
- case "doctor": {
802
- const { runDoctor } = await import("./doctor");
803
- await runDoctor(args.slice(1));
804
- break;
805
- }
806
- case "debug": {
807
- const { handleDebugCommand } = await import("./debug");
808
- await handleDebugCommand(args.slice(1));
809
- break;
810
- }
811
- case "ensure":
812
- await handleEnsure();
813
- break;
814
- case "login": {
815
- const { handleLogin } = await import("../oauth/login-cli");
816
- await handleLogin(args[1]);
817
- break;
818
- }
819
- case "logout": {
820
- const { removeCredential } = await import("../oauth/store");
821
- const name = (args[1] ?? "").trim().toLowerCase();
822
- await removeCredential(name);
823
- console.log(`Logged out of ${name || "(none)"}.`);
824
- break;
825
- }
826
- case "sync": {
827
- const restartCodex = args.slice(1).includes("--restart-codex");
828
- const synced = await syncModelsToCodex((await findLiveProxy())?.port);
829
- if (!synced.ok) {
830
- process.exitCode = 1;
831
- console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
832
- }
833
- // Only warn/restart when a catalog or models_cache write actually happened. This is
834
- // deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
835
- // so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
836
- // exactly when a long-lived app-server is holding the stale list.
837
- if (synced.catalogWritten || synced.cacheSynced) {
838
- const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
839
- afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
840
- }
841
- break;
842
- }
843
- case "v2": {
844
- const { cmdV2 } = await import("./v2");
845
- process.exitCode = await cmdV2(args.slice(1), {}, async () => (await findLiveProxy())?.port);
846
- break;
847
- }
848
- case "sync-cache": {
849
- const restartCodex = args.slice(1).includes("--restart-codex");
850
- const { invalidateCodexModelsCache } = await import("../codex/catalog");
851
- // Only warn/restart when models_cache was actually rewritten from a readable catalog.
852
- if (invalidateCodexModelsCache()) {
853
- const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
854
- afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
855
- }
856
- break;
857
- }
858
- case "gui": {
859
- const cfg = await import("../config");
860
- const config = cfg.loadConfig();
861
- // Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
862
- // proxy and waits until the spawned one actually answers before opening the browser.
863
- let live = await findLiveProxy();
864
- if (!live) {
865
- console.log("Proxy not running. Starting...");
866
- const child = spawn(process.execPath, startArgv((config.port ?? 10100) > 0 ? (config.port ?? 10100) : undefined), {
867
- detached: true,
868
- stdio: "ignore",
869
- windowsHide: true,
870
- env: process.env,
871
- });
872
- child.unref();
873
- live = await waitForProxy();
874
- if (!live) {
875
- console.error("❌ Proxy did not become healthy after starting. Not opening the GUI.");
876
- process.exit(1);
877
- }
878
- }
879
- // Open the host the proxy actually binds — `localhost` only answers for
880
- // loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
881
- const guiHost = probeHostname(live?.hostname ?? config.hostname);
882
- const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
883
- console.log(`Opening ${guiUrl}`);
884
- const { openUrl } = await import("../lib/open-url");
885
- openUrl(guiUrl);
886
- break;
887
- }
888
- case "service":
889
- await serviceCommand(...args.slice(1));
890
- break;
891
- case "tray": {
892
- const { windowsTrayCommand } = await import("../tray/windows");
893
- await windowsTrayCommand(args.slice(1));
894
- break;
895
- }
896
- case "codex-shim": {
897
- const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
898
- switch (args[1]) {
899
- case "install": {
900
- const r = installCodexShim();
901
- console.log(r.installed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
902
- break;
903
- }
904
- case "status":
905
- console.log(codexShimStatus());
906
- break;
907
- case "uninstall":
908
- case "remove": {
909
- const r = uninstallCodexShim();
910
- console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
911
- break;
912
- }
913
- default:
914
- console.error("Usage: ocx codex-shim <install|status|uninstall|remove>");
915
- process.exit(1);
916
- }
917
- break;
918
- }
919
- case "update": {
920
- // `ocx update --help` must print usage and exit WITHOUT side effects — running the
921
- // real self-update stops the proxy and drops in-flight routed streams (issue #168).
922
- if (hasHelpFlag(args.slice(1))) {
923
- printSubcommandUsage("update");
924
- break;
925
- }
926
- const { runUpdate } = await import("../update");
927
- await runUpdate();
928
- break;
929
- }
930
- case "__refresh-version": {
931
- // Hidden, detached helper spawned by the update prompt to refresh the
932
- // cached latest version without blocking the foreground start. Not in help.
933
- const { refreshVersionCache } = await import("../update/notify");
934
- const channel = args[1] === "preview" ? "preview" : "latest";
935
- await refreshVersionCache(channel);
936
- break;
937
- }
938
- case "__tray-start":
939
- case "__tray-restart":
940
- case "__startup-health":
941
- await dispatchInternalCliCommand(command as InternalCliCommand, {
942
- trayStart: handleTrayProxyStart,
943
- trayRestart: handleTrayProxyRestart,
944
- startupHealth: async () => {
945
- const { collectStartupHealth } = await import("../codex/autostart-health");
946
- console.log(JSON.stringify(collectStartupHealth(loadConfig())));
947
- },
948
- });
949
- break;
950
- case "__tray-host": {
951
- const { runWindowsTrayHost } = await import("../tray/windows");
952
- await runWindowsTrayHost();
953
- break;
954
- }
955
- case "__gui-update-worker": {
956
- const jobId = args[1];
957
- if (!jobId) process.exit(1);
958
- const channel = normalizeUpdateChannel(args[2]);
959
- await runGuiUpdateWorker(jobId, channel, args[3] === "restart");
960
- break;
961
- }
962
- case "restart": {
963
- // A failed stop must not be followed by a re-inject: with a foreign service still running
964
- // (ownership mismatch) we would rewrite shared config we just declined to touch.
965
- if (await handleStop()) await handleEnsure();
966
- else console.error("↩️ Restart aborted: the proxy was not stopped cleanly.");
967
- break;
968
- }
969
- case "health": {
970
- const healthArgs = args.slice(1);
971
- const wantsHealthJson = healthArgs.includes("--json");
972
- const live = await findLiveProxy();
973
- if (wantsHealthJson) {
974
- console.log(JSON.stringify({ ok: !!live, pid: live?.pid ?? null, port: live?.port ?? null }));
975
- } else {
976
- console.log(live ? `Proxy healthy (PID ${live.pid}, port ${live.port})` : "Proxy not healthy");
977
- }
978
- process.exit(live ? 0 : 1);
979
- }
980
- case "provider": {
981
- const { handleProviderCommand } = await import("./provider");
982
- await handleProviderCommand(args.slice(1));
983
- break;
984
- }
985
- case "account": {
986
- const { cmdAccount } = await import("./account");
987
- process.exitCode = await cmdAccount(args.slice(1));
988
- break;
989
- }
990
- case "models":
991
- case "model": {
992
- const { handleModels } = await import("./models");
993
- await handleModels(args.slice(1));
994
- break;
995
- }
996
- case "combo": {
997
- const { handleComboCommand } = await import("./combo");
998
- process.exitCode = await handleComboCommand(args.slice(1));
999
- break;
1000
- }
1001
- case "route": {
1002
- if (args[1] !== "combo") {
1003
- console.error("Usage: ocx route combo <subcommand>");
1004
- process.exitCode = 2;
1005
- break;
1006
- }
1007
- const { handleComboCommand } = await import("./combo");
1008
- process.exitCode = await handleComboCommand(args.slice(2));
1009
- break;
1010
- }
1011
- case "agent": {
1012
- const { handleAgentCommand } = await import("./agent");
1013
- process.exitCode = await handleAgentCommand(args.slice(1));
1014
- break;
1015
- }
1016
- case "observe": {
1017
- const { handleObserveCommand } = await import("./observe");
1018
- process.exitCode = await handleObserveCommand(args.slice(1));
1019
- break;
1020
- }
1021
- case "logs":
1022
- case "usage":
1023
- case "storage":
1024
- case "memory": {
1025
- const { handleObserveCommand } = await import("./observe");
1026
- process.exitCode = await handleObserveCommand([command, ...args.slice(1)]);
1027
- break;
1028
- }
1029
- case "access": {
1030
- const { handleAccessCommand } = await import("./access");
1031
- process.exitCode = await handleAccessCommand(args.slice(1));
1032
- break;
1033
- }
1034
- case "api-key": {
1035
- const { handleAccessCommand } = await import("./access");
1036
- process.exitCode = await handleAccessCommand(["key", ...args.slice(1)]);
1037
- break;
1038
- }
1039
- case "export": {
1040
- const { handleExportCommand } = await import("./export-command");
1041
- process.exitCode = await handleExportCommand(args.slice(1));
1042
- break;
1043
- }
1044
- case "grok": {
1045
- const { handleGrokCommand } = await import("./integrations");
1046
- process.exitCode = await handleGrokCommand(args.slice(1));
1047
- break;
1048
- }
1049
- case "integration": {
1050
- const integration = args[1];
1051
- if (integration === "grok") {
1052
- const { handleGrokCommand } = await import("./integrations");
1053
- process.exitCode = await handleGrokCommand(args.slice(2));
1054
- } else if (integration === "claude") {
1055
- const { handleClaudeConfigCommand } = await import("./integrations");
1056
- process.exitCode = await handleClaudeConfigCommand(args.slice(2));
1057
- } else {
1058
- console.error("Usage: ocx integration <claude|grok> <subcommand>");
1059
- process.exitCode = 2;
1060
- }
1061
- break;
1062
- }
1063
- case "system": {
1064
- const { handleSystemCommand } = await import("./system-command");
1065
- process.exitCode = await handleSystemCommand(args.slice(1));
1066
- break;
1067
- }
1068
- case "config": {
1069
- const { handleConfigCommand } = await import("./config-command");
1070
- process.exitCode = await handleConfigCommand(args.slice(1));
1071
- break;
1072
- }
1073
- case "sync-cloud": {
1074
- const { handleSyncCloudCommand } = await import("./sync-cloud");
1075
- process.exitCode = await handleSyncCloudCommand(args.slice(1));
1076
- break;
1077
- }
1078
- case "claude": {
1079
- const { cmdClaude } = await import("./claude");
1080
- // "ocx claude desktop" → write Desktop 3P config
1081
- if (args[1] === "desktop") {
1082
- const { handleClaudeDesktopCommand } = await import("./claude-desktop");
1083
- const exitCode = await handleClaudeDesktopCommand(args.slice(2));
1084
- if (exitCode !== 0) process.exit(exitCode);
1085
- break;
1086
- }
1087
- if (args[1] === "config") {
1088
- const { handleClaudeConfigCommand } = await import("./integrations");
1089
- process.exitCode = await handleClaudeConfigCommand(args.slice(2));
1090
- break;
1091
- }
1092
- process.exit(await cmdClaude(args.slice(1)));
1093
- }
1094
- case "opencode": {
1095
- const { cmdOpencode } = await import("./opencode");
1096
- process.exit(await cmdOpencode(args.slice(1)));
1097
- }
1098
- case "pi": {
1099
- const { handlePiCommand } = await import("./pi");
1100
- process.exitCode = await handlePiCommand(args.slice(1));
1101
- break;
1102
- }
1103
- case "help":
1104
- case "--help":
1105
- case "-h":
1106
- case undefined:
1107
- printUsage();
1108
- break;
1109
- default:
1110
- console.error(`Unknown command: ${command}`);
1111
- printUsage();
1112
- process.exit(1);
931
+ /**
932
+ * `ocx ready` — arguments are pre-parsed above (before
933
+ * maybeAutoRestoreCodexShim) so invalid usage exits 64 before any global
934
+ * preflight. This handler only runs the dependency-injected runner in ./ready
935
+ * and exits with the returned code; it performs no parsing and no I/O of its
936
+ * own. The full behavior is unit-testable without spawning a subprocess.
937
+ */
938
+ async function handleReady(args: ReadyArgs): Promise<number> {
939
+ return runReady(args);
1113
940
  }
941
+
942
+ process.exit(await dispatchCommand(head, {
943
+ args,
944
+ command,
945
+ head,
946
+ loadConfig,
947
+ findLiveProxy,
948
+ probeHostname,
949
+ waitForProxy,
950
+ startArgv,
951
+ spawnDetached: argv => {
952
+ const child = spawn(process.execPath, argv, {
953
+ detached: true,
954
+ stdio: "ignore",
955
+ windowsHide: true,
956
+ env: withProcessRuntimeProvenance(process.env),
957
+ });
958
+ child.unref();
959
+ },
960
+ handleStart,
961
+ handleStop,
962
+ handleEnsure,
963
+ handleTrayProxyStart,
964
+ handleTrayProxyRestart,
965
+ handleRestartStartWhenStopped,
966
+ handleProxyRestart,
967
+ handleUninstall,
968
+ handleStatus,
969
+ handleRecoverHistory,
970
+ handleReady,
971
+ serviceCommand,
972
+ }));