@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/codex/shim.ts CHANGED
@@ -1,25 +1,33 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { delimiter, dirname, extname, join, posix } from "node:path";
2
+ import { spawnSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, delimiter, dirname, extname, join, posix } from "node:path";
3
5
  import {
4
6
  chmodSync,
5
7
  closeSync,
6
8
  existsSync,
7
9
  fstatSync,
8
10
  lstatSync,
11
+ linkSync,
9
12
  mkdirSync,
13
+ mkdtempSync,
10
14
  openSync,
11
15
  readFileSync,
12
16
  readdirSync,
17
+ readlinkSync,
13
18
  readSync,
14
19
  renameSync,
20
+ rmSync,
15
21
  rmdirSync,
16
22
  statSync,
23
+ symlinkSync,
17
24
  type Stats,
18
25
  unlinkSync,
19
26
  writeFileSync,
20
27
  } from "node:fs";
21
28
  import { getConfigDir } from "../config";
22
- import { durableBunPath } from "../lib/bun-runtime";
29
+ import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "../lib/bun-runtime";
30
+ import type { BunRuntimeSource } from "../lib/bun-runtime";
23
31
  import { isProcessAlive } from "../lib/process-control";
24
32
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
25
33
  import { recordOwnedConfigPath } from "../lib/config-ownership";
@@ -28,10 +36,173 @@ import { isWslRuntime, wslAutomountRoot } from "./home";
28
36
  import { truncateRetainedUtf8 } from "../lib/admission";
29
37
 
30
38
  const SHIM_MARKER = "opencodex codex autostart shim";
39
+ const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
31
40
  const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
32
41
  export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
33
42
  export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
34
43
  const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
44
+ const CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS = 5_000;
45
+ const CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS = 1_000;
46
+ const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
47
+ const CODEX_SHIM_REENTRY_DIAGNOSTIC = "opencodex: saved Codex launcher resolved back to the autostart shim; run ocx codex-shim uninstall and reinstall Codex before enabling codexAutoStart.";
48
+ const CODEX_SHIM_INSTALL_PROBE_SCRIPT = `
49
+ const { spawn } = require("node:child_process");
50
+ const { readFileSync, writeFileSync } = require("node:fs");
51
+ const [markerPath, reentryPath, groupPath, stderrPath, launcherShellPath, wrapperPath, timeoutRaw, stderrLimitRaw, stderrDrainRaw, observationRaw] = process.argv.slice(1);
52
+ const timeoutMs = Number.parseInt(timeoutRaw, 10);
53
+ const stderrLimit = Number.parseInt(stderrLimitRaw, 10);
54
+ const stderrDrainMs = Number.parseInt(stderrDrainRaw, 10);
55
+ const observationMs = Number.parseInt(observationRaw, 10);
56
+ const probeStartedAt = Date.now();
57
+ const stderrChunks = [];
58
+ let stderrBytes = 0;
59
+ let launcher;
60
+ let probeLease;
61
+ let timer;
62
+ let stderrDrainTimer;
63
+ let observationTimer;
64
+ let reentryPollTimer;
65
+ let marker = "";
66
+ let finished = false;
67
+
68
+ function writeExclusive(path, value) {
69
+ writeFileSync(path, value, { flag: "wx", mode: 0o600 });
70
+ }
71
+
72
+ function appendStderr(value) {
73
+ if (stderrBytes >= stderrLimit) return;
74
+ const bytes = Buffer.from(value);
75
+ const retained = bytes.subarray(0, stderrLimit - stderrBytes);
76
+ stderrChunks.push(retained);
77
+ stderrBytes += retained.byteLength;
78
+ }
79
+
80
+ function groupAlive() {
81
+ if (!launcher || !launcher.pid) return false;
82
+ try {
83
+ process.kill(-launcher.pid, 0);
84
+ return true;
85
+ } catch (error) {
86
+ return error && error.code !== "ESRCH";
87
+ }
88
+ }
89
+
90
+ function killGroup() {
91
+ if (!launcher || !launcher.pid) return;
92
+ try { process.kill(-launcher.pid, "SIGKILL"); } catch (error) {
93
+ if (!error || error.code !== "ESRCH") appendStderr(String(error));
94
+ }
95
+ }
96
+
97
+ function setMarker(value) {
98
+ if (marker) return;
99
+ marker = value;
100
+ try { writeExclusive(markerPath, value + "\\n"); } catch (error) { appendStderr(String(error)); }
101
+ }
102
+
103
+ function reentryDetected() {
104
+ try { return readFileSync(reentryPath, "utf8").trim() === "recursive"; } catch { return false; }
105
+ }
106
+
107
+ function checkReentry() {
108
+ if (finished || !reentryDetected()) return;
109
+ setMarker("recursive");
110
+ killGroup();
111
+ finish(126);
112
+ }
113
+
114
+ function finish(status) {
115
+ if (finished) return;
116
+ finished = true;
117
+ if (timer) clearTimeout(timer);
118
+ if (stderrDrainTimer) clearTimeout(stderrDrainTimer);
119
+ if (observationTimer) clearTimeout(observationTimer);
120
+ if (reentryPollTimer) clearInterval(reentryPollTimer);
121
+ if (!marker && reentryDetected()) setMarker("recursive");
122
+ if (!marker && groupAlive()) {
123
+ setMarker("descendants");
124
+ killGroup();
125
+ }
126
+ try { writeExclusive(stderrPath, Buffer.concat(stderrChunks)); } catch { /* parent fails closed */ }
127
+ process.exit(marker === "timeout" ? 124 : marker === "descendants" ? 125 : marker === "recursive" ? 126 : status);
128
+ }
129
+
130
+ function finishAfterStderr(status) {
131
+ if (finished) return;
132
+ if (timer) {
133
+ clearTimeout(timer);
134
+ timer = undefined;
135
+ }
136
+ if (!launcher || !launcher.stderr || !probeLease) {
137
+ finish(status);
138
+ return;
139
+ }
140
+ let stderrEnded = launcher.stderr.readableEnded;
141
+ let leaseEnded = probeLease.readableEnded;
142
+ let observationElapsed = false;
143
+ const finishWhenReady = () => {
144
+ if (stderrEnded && leaseEnded && observationElapsed) finish(status);
145
+ };
146
+ launcher.stderr.once("end", () => {
147
+ stderrEnded = true;
148
+ finishWhenReady();
149
+ });
150
+ probeLease.once("end", () => {
151
+ leaseEnded = true;
152
+ finishWhenReady();
153
+ });
154
+ stderrDrainTimer = setTimeout(() => {
155
+ stderrEnded = true;
156
+ if (!marker && groupAlive()) {
157
+ setMarker("descendants");
158
+ killGroup();
159
+ finish(125);
160
+ return;
161
+ }
162
+ finishWhenReady();
163
+ }, stderrDrainMs);
164
+ const remainingObservationMs = Math.max(0, observationMs - (Date.now() - probeStartedAt));
165
+ observationTimer = setTimeout(() => {
166
+ observationElapsed = true;
167
+ if (!leaseEnded) {
168
+ setMarker(groupAlive() ? "descendants" : "timeout");
169
+ killGroup();
170
+ finish(marker === "descendants" ? 125 : 124);
171
+ return;
172
+ }
173
+ finishWhenReady();
174
+ }, remainingObservationMs);
175
+ finishWhenReady();
176
+ }
177
+
178
+ try {
179
+ launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
180
+ detached: true,
181
+ env: process.env,
182
+ stdio: ["ignore", "ignore", "pipe", "pipe"],
183
+ });
184
+ if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
185
+ probeLease = launcher.stdio[3];
186
+ if (!probeLease) throw new Error("Codex shim probe launcher has no descendant lease pipe");
187
+ writeExclusive(groupPath, String(launcher.pid) + "\\n");
188
+ launcher.stderr.on("data", appendStderr);
189
+ reentryPollTimer = setInterval(checkReentry, 10);
190
+ launcher.once("error", error => {
191
+ appendStderr(String(error));
192
+ finishAfterStderr(127);
193
+ });
194
+ launcher.once("exit", code => finishAfterStderr(Number.isInteger(code) ? code : 127));
195
+ timer = setTimeout(() => {
196
+ setMarker("timeout");
197
+ killGroup();
198
+ finish(124);
199
+ }, timeoutMs);
200
+ } catch (error) {
201
+ appendStderr(String(error));
202
+ killGroup();
203
+ finish(127);
204
+ }
205
+ `;
35
206
  const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
36
207
  let lastShimDiscoveryError: string | null = null;
37
208
  /** Last human-readable reason discovery returned null (exposed for doctor/tests). */
@@ -120,11 +291,13 @@ export type CodexShimAutoRestoreResult =
120
291
  | { status: "ineligible" | "deferred"; message?: string }
121
292
  | { status: "restored"; message: string };
122
293
 
123
- function cliEntry(): { bun: string; cli: string } {
294
+ function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
124
295
  // Bundled Bun path (survives `ocx update`); all three shim builders
125
296
  // (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
126
297
  // This module lives in src/codex/, the CLI entry in src/cli/index.ts.
127
- return { bun: durableBunPath(), cli: join(import.meta.dir, "..", "cli", "index.ts") };
298
+ // Path and provenance resolve together so the marker always describes this binary.
299
+ const runtime = durableBunRuntime();
300
+ return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "..", "cli", "index.ts") };
128
301
  }
129
302
 
130
303
  function commandNames(name: string): string[] {
@@ -145,6 +318,7 @@ function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
145
318
  try {
146
319
  const content = readFileSync(path, "utf8");
147
320
  if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
321
+ if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
148
322
  if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
149
323
  return true;
150
324
  } catch {
@@ -201,6 +375,12 @@ function sameFingerprint(
201
375
  : false);
202
376
  }
203
377
 
378
+ function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
379
+ // rename changes the outer directory entry ctime on macOS; every other field,
380
+ // including a symlink target fingerprint, must remain identical.
381
+ return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
382
+ }
383
+
204
384
  function stableShimPathProbe(path: string): StableShimPathProbe | null {
205
385
  const before = statFingerprint(path, false);
206
386
  if (!before) return null;
@@ -230,12 +410,73 @@ function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPat
230
410
  return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
231
411
  }
232
412
 
413
+ /**
414
+ * Identity of whatever sits at `path`, read from metadata alone.
415
+ *
416
+ * `stableShimPathProbe` answers a different question: it reads content to decide
417
+ * whether a launcher looks like a healthy shim, and it deliberately returns null
418
+ * for a zero-byte file. That makes it the wrong instrument for rollback
419
+ * bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
420
+ * install moves it aside before writing our wrapper; if the move is recorded
421
+ * without a fingerprint, rollback cannot prove the backup is still the file it
422
+ * set aside and refuses to restore it — the launcher stays lost (#1625).
423
+ *
424
+ * Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
425
+ * re-reads them to reject a path that changed under us, following a symlink to
426
+ * fingerprint its target as well.
427
+ */
428
+ function shimPathFingerprint(path: string): ShimPathFingerprint | null {
429
+ const before = statFingerprint(path, false);
430
+ if (!before) return null;
431
+ if (before.kind !== "symlink") {
432
+ const after = statFingerprint(path, false);
433
+ return after && sameFingerprint(before, after) ? before : null;
434
+ }
435
+ const targetBefore = statFingerprint(path, true);
436
+ if (!targetBefore) return null;
437
+ const targetAfter = statFingerprint(path, true);
438
+ const after = statFingerprint(path, false);
439
+ if (!targetAfter || !after
440
+ || !sameFingerprint(targetBefore, targetAfter)
441
+ || !sameFingerprint(before, after)) return null;
442
+ return { ...before, target: targetBefore };
443
+ }
444
+
445
+ /**
446
+ * Move `from` onto `to` without ever replacing an existing entry.
447
+ *
448
+ * `renameSync` silently clobbers the destination on POSIX, which is wrong for a
449
+ * rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
450
+ * a concurrent installer can publish its own launcher at the original path in
451
+ * between, and the restore would delete it. `link` fails EEXIST instead, which
452
+ * is the no-replace primitive we need and needs no native helper.
453
+ *
454
+ * `link` follows a symlink to its target rather than preserving the link, so a
455
+ * symlink launcher is republished with `symlink`, which is also no-replace: it
456
+ * fails EEXIST on an occupied destination. Checking existence and then renaming
457
+ * would reintroduce exactly the race this function exists to close.
458
+ */
459
+ function restoreWithoutReplacing(from: string, to: string): void {
460
+ const source = lstatSync(from);
461
+ if (source.isSymbolicLink()) {
462
+ symlinkSync(readlinkSync(from), to);
463
+ unlinkSync(from);
464
+ return;
465
+ }
466
+ linkSync(from, to);
467
+ unlinkSync(from);
468
+ }
469
+
233
470
  function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
234
471
  if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
235
472
  const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
236
473
  return platform === "win32" || (mode & 0o111) !== 0;
237
474
  }
238
475
 
476
+ function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
477
+ return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
478
+ }
479
+
239
480
  function hasUsableBackingPath(file: ShimFileState): boolean {
240
481
  return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
241
482
  .some(path => {
@@ -366,11 +607,54 @@ function shQuote(value: string): string {
366
607
  return `'${value.replace(/'/g, "'\\''")}'`;
367
608
  }
368
609
 
369
- export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, tokenFile = serviceApiTokenFilePath()): string {
610
+ // Provenance is required rather than defaulted: a default would let a caller pass an
611
+ // override binary and silently label it something else, which is precisely the
612
+ // path/marker disagreement this feature exists to prevent.
613
+ //
614
+ // The marker is scoped to the `ensure` invocation in every flavor below and is never
615
+ // exported into the shim's own environment. A shim wraps the real `codex`, so an
616
+ // exported marker would be inherited by Codex and everything it spawns — a shell that
617
+ // then ran a *different* Bun directly would carry a provenance describing a binary it
618
+ // is not executing.
619
+ export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
370
620
  const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
371
621
  const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
372
622
  return `#!/usr/bin/env sh
373
623
  # ${SHIM_MARKER}
624
+ # ${UNIX_SHIM_REVISION_MARKER}
625
+ if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
626
+ if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
627
+ if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
628
+ (umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
629
+ fi
630
+ printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
631
+ exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
632
+ fi
633
+ OCX_SHIM_PROBE_ACTIVE=1
634
+ export OCX_SHIM_PROBE_ACTIVE
635
+ fi
636
+ if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
637
+ printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
638
+ exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
639
+ fi
640
+ case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
641
+ 0)
642
+ OCX_SHIM_ACTIVE_DEPTH=1
643
+ ;;
644
+ 1)
645
+ OCX_SHIM_ACTIVE_DEPTH=2
646
+ ;;
647
+ *)
648
+ printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
649
+ exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
650
+ ;;
651
+ esac
652
+ # Dynamic launchers such as mise exec -- codex may resolve the command name
653
+ # back to this wrapper. An exec chain keeps the same PID. A legitimate nested
654
+ # Codex invocation may enter once with a new PID; repeated child-process
655
+ # redispatch reaches depth 2 and is rejected before it can form an infinite chain.
656
+ OCX_SHIM_ACTIVE_PID=$$
657
+ export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
374
658
  if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
375
659
  OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
376
660
  export OPENCODEX_API_AUTH_TOKEN
@@ -406,7 +690,7 @@ case "$ocx_subcommand" in
406
690
  ;;
407
691
  *)
408
692
  if [ -z "$OCX_SHIM_BYPASS" ]; then
409
- ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
693
+ ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
410
694
  fi
411
695
  ;;
412
696
  esac
@@ -414,6 +698,239 @@ exec ${shQuote(realCodexPath)} "$@"
414
698
  `;
415
699
  }
416
700
 
701
+ type UnixShimProbeResult = "cleanup" | "descendants" | "failed" | "recursive" | "timeout" | null;
702
+
703
+ let codexShimProbeHookForTests: (() => void) | null = null;
704
+ let codexShimProbeShellForTests: string | null = null;
705
+ let codexShimGuardedWriteHookForTests: (() => void) | null = null;
706
+ let codexShimFreshWriteHookForTests: (() => void) | null = null;
707
+ let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
708
+ let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
709
+
710
+ /** Narrow deterministic seam for transaction rollback tests. */
711
+ export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
712
+ codexShimProbeHookForTests = hook;
713
+ }
714
+
715
+ /** Selects a POSIX shell only for cross-shell probe regression tests. */
716
+ export function setCodexShimProbeShellForTests(path: string | null): void {
717
+ codexShimProbeShellForTests = path;
718
+ }
719
+
720
+ /** Shortens the successful-launcher observation window only for focused tests. */
721
+ export function setCodexShimProbeObservationMsForTests(value: number | null): void {
722
+ codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
723
+ }
724
+
725
+ /** Narrow deterministic seam for guarded partial-write rollback tests. */
726
+ export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
727
+ codexShimGuardedWriteHookForTests = hook;
728
+ }
729
+
730
+ /** Narrow deterministic seam for fresh-install partial-write rollback tests. */
731
+ export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): void {
732
+ codexShimFreshWriteHookForTests = hook;
733
+ }
734
+
735
+ /**
736
+ * @internal Test-only seam for the rollback restore race.
737
+ *
738
+ * The window this closes opens after `sourceOccupied` is sampled and closes when
739
+ * the backup is republished, so no earlier hook can reach it: publishing from
740
+ * the fresh-write hook makes `sourceOccupied` true and skips the restore
741
+ * entirely.
742
+ */
743
+ export function setCodexShimRollbackRestoreHookForTests(
744
+ hook: ((target: ShimFileState) => void) | null,
745
+ ): void {
746
+ codexShimRollbackRestoreHookForTests = hook;
747
+ }
748
+
749
+ function readProbeMetadata(path: string, maxBytes: number): string | null {
750
+ try {
751
+ if (!existsSync(path)) return "";
752
+ const stat = lstatSync(path);
753
+ if (!stat.isFile() || stat.size > maxBytes) return null;
754
+ return readFileSync(path, "utf8").trim();
755
+ } catch {
756
+ return null;
757
+ }
758
+ }
759
+
760
+ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
761
+ if (process.platform === "win32") return null;
762
+ const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
763
+ const markerPath = join(probeDir, "result");
764
+ const reentryPath = join(probeDir, "reentry");
765
+ const groupPath = join(probeDir, "group");
766
+ const stderrPath = join(probeDir, "stderr");
767
+ const env: NodeJS.ProcessEnv = {
768
+ ...process.env,
769
+ OCX_SHIM_BYPASS: "1",
770
+ OCX_SHIM_PROBE: "1",
771
+ OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
772
+ };
773
+ delete env.OCX_SHIM_ACTIVE_PID;
774
+ delete env.OCX_SHIM_ACTIVE_DEPTH;
775
+ delete env.OCX_SHIM_PROBE_ACTIVE;
776
+ let groupId = 0;
777
+ try {
778
+ chmodSync(probeDir, 0o700);
779
+ const result = spawnSync(process.execPath, [
780
+ "-e",
781
+ CODEX_SHIM_INSTALL_PROBE_SCRIPT,
782
+ markerPath,
783
+ reentryPath,
784
+ groupPath,
785
+ stderrPath,
786
+ codexShimProbeShellForTests ?? "/bin/sh",
787
+ wrapperPath,
788
+ String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
789
+ String(MAX_DIAGNOSTIC_VALUE_BYTES),
790
+ String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
791
+ String(codexShimProbeObservationMs),
792
+ ], {
793
+ encoding: "utf8",
794
+ env,
795
+ timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
796
+ killSignal: "SIGKILL",
797
+ });
798
+ const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
799
+ const marker = readProbeMetadata(markerPath, 64);
800
+ const reentryMarker = readProbeMetadata(reentryPath, 64);
801
+ const groupText = readProbeMetadata(groupPath, 64);
802
+ const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
803
+ groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
804
+ if (marker === null || reentryMarker === null || groupText === null || launcherStderr === null
805
+ || !Number.isInteger(groupId) || groupId <= 0) return "cleanup";
806
+ const groupSurvived = unixProcessGroupAlive(groupId);
807
+ if (timedOut || marker || reentryMarker || groupSurvived) {
808
+ try {
809
+ terminateUnixProcessGroup(groupId);
810
+ } catch {
811
+ return "cleanup";
812
+ }
813
+ }
814
+ if (result.error && !timedOut) return "cleanup";
815
+ if (timedOut || marker === "timeout") return "timeout";
816
+ if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
817
+ if (reentryMarker !== "") return "cleanup";
818
+ if (marker === "descendants") return "descendants";
819
+ if (groupSurvived) return "descendants";
820
+ if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
821
+ return "recursive";
822
+ }
823
+ if (result.status !== 0) return "failed";
824
+ return null;
825
+ } catch {
826
+ if (Number.isInteger(groupId) && groupId > 0) {
827
+ try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
828
+ }
829
+ return "cleanup";
830
+ } finally {
831
+ try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
832
+ }
833
+ }
834
+
835
+ function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
836
+ if (process.platform === "win32") return null;
837
+ codexShimProbeHookForTests?.();
838
+ return files
839
+ .filter(file => !file.preserveOnly)
840
+ .map(file => probeUnixShimInstall(file.wrapperPath))
841
+ .find(result => result !== null) ?? null;
842
+ }
843
+
844
+ function unixProcessGroupAlive(groupId: number): boolean {
845
+ try {
846
+ process.kill(-groupId, 0);
847
+ return true;
848
+ } catch (error) {
849
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
850
+ }
851
+ }
852
+
853
+ function terminateUnixProcessGroup(groupId: number): void {
854
+ try {
855
+ process.kill(-groupId, "SIGKILL");
856
+ } catch (error) {
857
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
858
+ }
859
+ const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
860
+ while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
861
+ if (unixProcessGroupAlive(groupId)) {
862
+ throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
863
+ }
864
+ }
865
+
866
+ interface FreshShimInstallJournalEntry {
867
+ target: ShimFileState;
868
+ movedOriginalFingerprint?: ShimPathFingerprint;
869
+ originalMovedToBackup: boolean;
870
+ writtenWrapperFingerprint?: ShimPathFingerprint;
871
+ wrapperWriteStarted: boolean;
872
+ /** dev/ino of the file our `writeShim()` created, recorded before anything can fail. */
873
+ writtenWrapperInode?: { dev: number; ino: number };
874
+ }
875
+
876
+ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry[]): void {
877
+ const errors: Error[] = [];
878
+ for (const entry of [...journal].reverse()) {
879
+ const target = entry.target;
880
+ let sourceOccupied = false;
881
+ let ownsWrapperNow = false;
882
+ try {
883
+ const wrapper = stableShimPathProbe(target.wrapperPath);
884
+ // Ownership is the inode our own write created. There is no marker-text
885
+ // fallback: the markers are public, so a concurrent updater's wrapper carries
886
+ // them too, and treating that as proof is how we would delete a file we never
887
+ // wrote. An in-place truncation of our file keeps the inode and is still ours
888
+ // to clean up; a replacement renamed over it has a different inode and is not.
889
+ const ownsWrapper = !target.preserveOnly
890
+ && entry.wrapperWriteStarted
891
+ && wrapper !== null
892
+ && wrapperInodeIsOurs(wrapper, entry.writtenWrapperInode, entry.writtenWrapperFingerprint);
893
+ ownsWrapperNow = ownsWrapper;
894
+ if (ownsWrapper) unlinkSync(target.wrapperPath);
895
+ else {
896
+ try {
897
+ lstatSync(target.originalPath);
898
+ sourceOccupied = true;
899
+ } catch (error) {
900
+ if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
901
+ }
902
+ }
903
+ } catch (error) {
904
+ errors.push(error instanceof Error ? error : new Error(String(error)));
905
+ }
906
+ try {
907
+ if (entry.originalMovedToBackup && existsSync(target.backupPath)) {
908
+ const movedOriginal = shimPathFingerprint(target.backupPath);
909
+ if (!movedOriginal || !entry.movedOriginalFingerprint
910
+ || !sameFingerprint(movedOriginal, entry.movedOriginalFingerprint)) {
911
+ throw new Error("Codex shim fresh-install backup changed during rollback");
912
+ }
913
+ if (sourceOccupied) {
914
+ // Something else occupies the source path. Dropping the backup is correct
915
+ // only when that something is a file we own; when a concurrent updater
916
+ // owns it, this backup is the user's real launcher and deleting it would
917
+ // lose the command entirely. Keep it in that case — a stray
918
+ // `codex.opencodex-real` is recoverable, a deleted launcher is not.
919
+ if (ownsWrapperNow) unlinkSync(target.backupPath);
920
+ } else {
921
+ // No-replace: sourceOccupied was sampled earlier, so a concurrent
922
+ // installer may have published a launcher at the original path since.
923
+ codexShimRollbackRestoreHookForTests?.(target);
924
+ restoreWithoutReplacing(target.backupPath, target.originalPath);
925
+ }
926
+ }
927
+ } catch (error) {
928
+ errors.push(error instanceof Error ? error : new Error(String(error)));
929
+ }
930
+ }
931
+ if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
932
+ }
933
+
417
934
  function windowsBatchValue(value: string): string {
418
935
  return value
419
936
  .replace(/%/g, "%%")
@@ -431,7 +948,7 @@ function windowsBatchSet(name: string, value: string): string {
431
948
  return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
432
949
  }
433
950
 
434
- export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
951
+ export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
435
952
  const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
436
953
  const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
437
954
  return `@echo off\r
@@ -462,7 +979,11 @@ if "%~1"=="" goto ensure_ocx\r
462
979
  shift\r
463
980
  goto scan_codex_args\r
464
981
  :ensure_ocx\r
982
+ setlocal\r
983
+ ${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
984
+ ${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
465
985
  "%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
986
+ endlocal\r
466
987
  :run_codex\r
467
988
  "%OCX_REAL_CODEX%" %*\r
468
989
  `;
@@ -472,7 +993,7 @@ function psString(value: string): string {
472
993
  return `'${value.replace(/'/g, "''")}'`;
473
994
  }
474
995
 
475
- export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
996
+ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
476
997
  const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
477
998
  const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
478
999
  const tokenFile = serviceApiTokenFilePath();
@@ -497,7 +1018,17 @@ foreach ($argValue in $args) {
497
1018
  }
498
1019
  $skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
499
1020
  if (-not $skipEnsure) {
500
- & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null
1021
+ $priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
1022
+ $priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
1023
+ $env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
1024
+ $env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
1025
+ try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
1026
+ finally {
1027
+ if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
1028
+ else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
1029
+ if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
1030
+ else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
1031
+ }
501
1032
  }
502
1033
  & ${psString(realCodexPath)} @args
503
1034
  exit $LASTEXITCODE
@@ -600,30 +1131,110 @@ function gitBashPath(path: string): string {
600
1131
  return path.replace(/\\/g, "/");
601
1132
  }
602
1133
 
603
- function writeShim(wrapperPath: string, realCodexPath: string): void {
604
- const { bun, cli } = cliEntry();
1134
+ /**
1135
+ * Write the wrapper and return the identity of the inode this call created, or
1136
+ * `undefined` where the platform still writes the destination in place.
1137
+ *
1138
+ * Callers must derive ownership from the returned identity rather than from a
1139
+ * later `stat` of `wrapperPath`: the shim markers are public, so a concurrent
1140
+ * updater's wrapper can carry them, and a replacement landing between the write
1141
+ * and the observation is otherwise indistinguishable from our own file.
1142
+ */
1143
+ function writeShim(wrapperPath: string, realCodexPath: string): { dev: number; ino: number } | undefined {
1144
+ const { bun, bunRuntimeSource, cli } = cliEntry();
605
1145
  if (process.platform === "win32") {
606
1146
  const lower = wrapperPath.toLowerCase();
607
1147
  if (lower.endsWith(".ps1")) {
608
1148
  // UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
609
1149
  // codepage, which mangles non-ASCII paths embedded in the shim.
610
- writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
1150
+ writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
611
1151
  } else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
612
- writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
1152
+ writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
613
1153
  } else {
614
1154
  // Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
615
1155
  writeFileSync(
616
1156
  wrapperPath,
617
- buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
1157
+ buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), bunRuntimeSource, gitBashPath(serviceApiTokenFilePath())),
618
1158
  "utf8",
619
1159
  );
620
1160
  }
1161
+ return undefined;
621
1162
  } else {
622
- writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
623
- chmodSync(wrapperPath, 0o755);
1163
+ // Stage the wrapper as its own inode and rename it into place, so ownership
1164
+ // comes from the write itself rather than from observing the path afterwards.
1165
+ // Writing the destination directly leaves a window in which a concurrent
1166
+ // updater can replace the file between our write and our fingerprint; we would
1167
+ // then adopt that replacement as ours and unlink it during rollback, deleting
1168
+ // an executable we never wrote.
1169
+ // Hidden and non-executable while staged, so a crash between the write and the
1170
+ // rename cannot leave an executable `codex*` artifact that a glob or a shell
1171
+ // completion would surface.
1172
+ const staged = join(dirname(wrapperPath), `.${basename(wrapperPath)}.opencodex-staging.${process.pid}.${randomUUID()}`);
1173
+ let renamed = false;
1174
+ try {
1175
+ // "wx" fails if the staging path somehow exists, so we never inherit a file.
1176
+ writeFileSync(staged, buildUnixCodexShim(realCodexPath, bun, cli, bunRuntimeSource), { encoding: "utf8", flag: "wx", mode: 0o600 });
1177
+ const stagedStat = lstatSync(staged);
1178
+ chmodSync(staged, 0o755);
1179
+ renameSync(staged, wrapperPath);
1180
+ renamed = true;
1181
+ // rename() preserves dev/ino and updates ctime, so identity is the inode
1182
+ // pair, captured from the file we created rather than from the destination.
1183
+ return { dev: stagedStat.dev, ino: stagedStat.ino };
1184
+ } finally {
1185
+ if (!renamed) {
1186
+ try { unlinkSync(staged); } catch { /* best-effort: nothing to clean up */ }
1187
+ }
1188
+ }
624
1189
  }
625
1190
  }
626
1191
 
1192
+ /**
1193
+ * The fingerprint to record as "we wrote this", or `undefined` when the file at
1194
+ * `wrapperPath` is not the inode `writeShim()` created. Returning `undefined`
1195
+ * makes every rollback path treat the file as someone else's and leave it alone.
1196
+ */
1197
+ function ownedWrapperFingerprint(
1198
+ wrapperPath: string,
1199
+ written: { dev: number; ino: number } | undefined,
1200
+ ): ShimPathFingerprint | undefined {
1201
+ const probe = stableShimPathProbe(wrapperPath);
1202
+ if (!probe) return undefined;
1203
+ // Platforms that still write in place (Windows) have no staged identity; fall
1204
+ // back to the marker check they have always used.
1205
+ if (!written) return probe.prefix.includes(SHIM_MARKER) ? probe.fingerprint : undefined;
1206
+ if (probe.fingerprint.dev !== written.dev || probe.fingerprint.ino !== written.ino) return undefined;
1207
+ return probe.fingerprint;
1208
+ }
1209
+
1210
+ /**
1211
+ * Whether the file now at the wrapper path is one this transaction may unlink.
1212
+ *
1213
+ * The inode our write created is the authority. It stays ours through an in-place
1214
+ * truncation — a partial write we must clean up — and stops being ours the moment
1215
+ * someone renames a different file over the path, which is exactly the concurrent
1216
+ * updater we must not delete. Where no inode was recorded (Windows writes the
1217
+ * destination directly), fall back to the exact fingerprint recorded at the time.
1218
+ */
1219
+ function wrapperInodeIsOurs(
1220
+ wrapper: StableShimPathProbe,
1221
+ written: { dev: number; ino: number } | undefined,
1222
+ recorded: ShimPathFingerprint | undefined,
1223
+ ): boolean {
1224
+ // A different inode is conclusive: someone renamed their own file over ours.
1225
+ if (written && (wrapper.fingerprint.dev !== written.dev || wrapper.fingerprint.ino !== written.ino)) {
1226
+ return false;
1227
+ }
1228
+ // Same inode is not sufficient on its own — an in-place truncation (shell `>`,
1229
+ // writeFileSync) keeps it while replacing the contents. When we recorded a full
1230
+ // fingerprint, require it to still match; that covers our own partial write,
1231
+ // whose fingerprint we record before anything can fail.
1232
+ if (recorded !== undefined) return sameFingerprint(wrapper.fingerprint, recorded);
1233
+ // No recorded fingerprint: only the inode identity we captured at write time can
1234
+ // speak for us, and there is nothing else to distinguish this file.
1235
+ return written !== undefined;
1236
+ }
1237
+
627
1238
  function stateFiles(state: ShimState): ShimFileState[] {
628
1239
  return state.wrappers?.length
629
1240
  ? state.wrappers
@@ -658,12 +1269,37 @@ function refreshShimFile(file: ShimFileState): boolean {
658
1269
  }
659
1270
  if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
660
1271
  if (file.wrapperPath !== file.originalPath) return false;
661
- replaceOwnedBackup(file.wrapperPath, file.backupPath);
662
- writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
663
- return true;
1272
+ const replacement = stableShimPathProbe(file.wrapperPath);
1273
+ if (!replacement) return false;
1274
+ return applyGuardedRefreshTransaction([{
1275
+ file,
1276
+ expectedReplacement: replacement.fingerprint,
1277
+ sourcePath: file.wrapperPath,
1278
+ }]);
664
1279
  }
665
1280
  if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
666
1281
  writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
1282
+ const writtenWrapper = stableShimPathProbe(file.wrapperPath);
1283
+ if (!writtenWrapper || !writtenWrapper.prefix.includes(SHIM_MARKER)) {
1284
+ return false;
1285
+ }
1286
+ let unsafe: UnixShimProbeResult = null;
1287
+ let probeError: Error | null = null;
1288
+ try {
1289
+ unsafe = probeUnixShimFiles([file]);
1290
+ } catch (error) {
1291
+ probeError = error instanceof Error ? error : new Error(String(error));
1292
+ }
1293
+ const currentWrapper = stableShimPathProbe(file.wrapperPath);
1294
+ const wrapperChangedDuringProbe = !currentWrapper
1295
+ || !sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint);
1296
+ if (unsafe !== null || probeError || wrapperChangedDuringProbe) {
1297
+ if (currentWrapper && sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint)) {
1298
+ unlinkSync(file.wrapperPath);
1299
+ }
1300
+ if (probeError) throw probeError;
1301
+ return false;
1302
+ }
667
1303
  return true;
668
1304
  }
669
1305
  if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
@@ -683,7 +1319,9 @@ interface GuardedRefreshOperation {
683
1319
  interface GuardedRefreshJournalEntry {
684
1320
  operation: GuardedRefreshOperation;
685
1321
  stagedOldBackupPath?: string;
1322
+ movedReplacementFingerprint?: ShimPathFingerprint;
686
1323
  replacementMovedToBackup: boolean;
1324
+ writtenWrapperFingerprint?: ShimPathFingerprint;
687
1325
  wrapperWriteStarted: boolean;
688
1326
  }
689
1327
 
@@ -875,14 +1513,35 @@ function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]):
875
1513
  }
876
1514
  };
877
1515
  for (const entry of [...journal].reverse()) {
1516
+ let sourceOccupied = false;
878
1517
  attempt(() => {
879
- if (entry.wrapperWriteStarted && existsSync(entry.operation.file.wrapperPath)) {
1518
+ const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
1519
+ // No marker-text fallback: the markers are public, so a concurrent updater's
1520
+ // wrapper carries them too. No recorded inode identity means not ours.
1521
+ const ownsWrapper = entry.wrapperWriteStarted
1522
+ && wrapper !== null
1523
+ && entry.writtenWrapperFingerprint !== undefined
1524
+ && sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
1525
+ if (ownsWrapper) {
880
1526
  unlinkSync(entry.operation.file.wrapperPath);
1527
+ } else {
1528
+ try {
1529
+ lstatSync(entry.operation.sourcePath);
1530
+ sourceOccupied = true;
1531
+ } catch (error) {
1532
+ if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
1533
+ }
881
1534
  }
882
1535
  });
883
1536
  attempt(() => {
884
1537
  if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
885
- renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
1538
+ const movedReplacement = stableShimPathProbe(entry.operation.file.backupPath);
1539
+ if (!movedReplacement || !entry.movedReplacementFingerprint
1540
+ || !sameFingerprint(movedReplacement.fingerprint, entry.movedReplacementFingerprint)) {
1541
+ throw new Error("Codex shim guarded refresh backup changed during rollback");
1542
+ }
1543
+ if (sourceOccupied) unlinkSync(entry.operation.file.backupPath);
1544
+ else renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
886
1545
  }
887
1546
  });
888
1547
  attempt(() => {
@@ -902,6 +1561,8 @@ function applyGuardedRefreshTransaction(
902
1561
  const journal: GuardedRefreshJournalEntry[] = [];
903
1562
  let applyError: Error | null = null;
904
1563
  let fingerprintMismatch = false;
1564
+ let unsafeLauncher = false;
1565
+ let wrapperChangedDuringProbe = false;
905
1566
  const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
906
1567
 
907
1568
  for (const [index, operation] of operations.entries()) {
@@ -925,15 +1586,43 @@ function applyGuardedRefreshTransaction(
925
1586
  }
926
1587
  renameSync(operation.sourcePath, operation.file.backupPath);
927
1588
  entry.replacementMovedToBackup = true;
1589
+ const movedReplacement = stableShimPathProbe(operation.file.backupPath);
1590
+ if (!movedReplacement) throw new Error("Codex shim guarded refresh could not fingerprint the staged launcher");
1591
+ entry.movedReplacementFingerprint = movedReplacement.fingerprint;
928
1592
  entry.wrapperWriteStarted = true;
929
- writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
1593
+ const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
1594
+ // Claim our own partial write before the hook can fail (see fresh install).
1595
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
1596
+ codexShimGuardedWriteHookForTests?.();
1597
+ // Re-check: unset means a concurrent writer owns the path now, so rollback
1598
+ // must leave it alone.
1599
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
1600
+ if (!entry.writtenWrapperFingerprint && !writtenInode) {
1601
+ throw new Error("Codex shim guarded refresh could not fingerprint the generated wrapper");
1602
+ }
930
1603
  } catch (error) {
931
1604
  applyError = error instanceof Error ? error : new Error(String(error));
932
1605
  break;
933
1606
  }
934
1607
  }
935
1608
 
936
- if (!fingerprintMismatch && !applyError && commitState) {
1609
+ if (!fingerprintMismatch && !applyError) {
1610
+ try {
1611
+ unsafeLauncher = probeUnixShimFiles(operations.map(operation => operation.file)) !== null;
1612
+ } catch (error) {
1613
+ applyError = error instanceof Error ? error : new Error(String(error));
1614
+ }
1615
+ }
1616
+
1617
+ if (!fingerprintMismatch && !applyError && !unsafeLauncher) {
1618
+ wrapperChangedDuringProbe = journal.some(entry => {
1619
+ const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
1620
+ return !wrapper || !entry.writtenWrapperFingerprint
1621
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
1622
+ });
1623
+ }
1624
+
1625
+ if (!fingerprintMismatch && !applyError && !unsafeLauncher && !wrapperChangedDuringProbe && commitState) {
937
1626
  try {
938
1627
  commitState();
939
1628
  } catch (error) {
@@ -941,7 +1630,7 @@ function applyGuardedRefreshTransaction(
941
1630
  }
942
1631
  }
943
1632
 
944
- if (fingerprintMismatch || applyError) {
1633
+ if (fingerprintMismatch || applyError || unsafeLauncher || wrapperChangedDuringProbe) {
945
1634
  const rollbackErrors = rollbackGuardedRefresh(journal);
946
1635
  if (applyError || rollbackErrors.length > 0) {
947
1636
  throw new AggregateError(
@@ -964,10 +1653,186 @@ function applyGuardedRefreshTransaction(
964
1653
  return true;
965
1654
  }
966
1655
 
1656
+ interface ObsoleteUnixShimJournalEntry {
1657
+ file: ShimFileState;
1658
+ stagedWrapperPath: string;
1659
+ priorWrapperFingerprint: ShimPathFingerprint;
1660
+ backingFingerprint: ShimPathFingerprint;
1661
+ writtenWrapperFingerprint?: ShimPathFingerprint;
1662
+ wrapperWriteStarted: boolean;
1663
+ }
1664
+
1665
+ function rollbackObsoleteUnixShimRefresh(journal: readonly ObsoleteUnixShimJournalEntry[]): Error[] {
1666
+ const errors: Error[] = [];
1667
+ const attempt = (operation: () => void): void => {
1668
+ try {
1669
+ operation();
1670
+ } catch (error) {
1671
+ errors.push(error instanceof Error ? error : new Error(String(error)));
1672
+ }
1673
+ };
1674
+ for (const entry of [...journal].reverse()) {
1675
+ attempt(() => {
1676
+ const wrapper = stableShimPathProbe(entry.file.wrapperPath);
1677
+ const ownsWrapper = entry.wrapperWriteStarted
1678
+ && wrapper !== null
1679
+ && (entry.writtenWrapperFingerprint
1680
+ ? sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
1681
+ : wrapper.prefix.includes(UNIX_SHIM_REVISION_MARKER));
1682
+ if (ownsWrapper) unlinkSync(entry.file.wrapperPath);
1683
+ });
1684
+ attempt(() => {
1685
+ if (!existsSync(entry.stagedWrapperPath)) return;
1686
+ if (existsSync(entry.file.wrapperPath)) unlinkSync(entry.stagedWrapperPath);
1687
+ else renameSync(entry.stagedWrapperPath, entry.file.wrapperPath);
1688
+ });
1689
+ }
1690
+ return errors;
1691
+ }
1692
+
1693
+ type ObsoleteUnixShimRefreshResult =
1694
+ | { installed: true; message: string }
1695
+ | { installed: false; deferred: boolean; message: string };
1696
+
1697
+ function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnixShimRefreshResult {
1698
+ if (process.platform === "win32") {
1699
+ return { installed: false, deferred: false, message: "Codex autostart shim is already current." };
1700
+ }
1701
+ const candidates = files.filter(file => {
1702
+ if (file.preserveOnly || file.wrapperPath !== file.originalPath || !existsSync(file.wrapperPath)) return false;
1703
+ const probe = stableShimPathProbe(file.wrapperPath);
1704
+ return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
1705
+ });
1706
+ if (candidates.length === 0) {
1707
+ return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
1708
+ }
1709
+
1710
+ const journal: ObsoleteUnixShimJournalEntry[] = [];
1711
+ const transactionId = `${process.pid}-${randomUUID()}`;
1712
+ let applyError: Error | null = null;
1713
+ for (const [index, file] of candidates.entries()) {
1714
+ const wrapper = stableShimPathProbe(file.wrapperPath);
1715
+ const backing = stableShimPathProbe(file.backupPath);
1716
+ if (!wrapper || !wrapper.prefix.includes(SHIM_MARKER) || isCurrentUnixShimProbe(wrapper) || !backing) {
1717
+ applyError = new Error("Codex autostart shim upgrade inputs changed before regeneration");
1718
+ break;
1719
+ }
1720
+ const entry: ObsoleteUnixShimJournalEntry = {
1721
+ file,
1722
+ stagedWrapperPath: `${file.wrapperPath}.upgrade-${transactionId}-${index}`,
1723
+ priorWrapperFingerprint: wrapper.fingerprint,
1724
+ backingFingerprint: backing.fingerprint,
1725
+ wrapperWriteStarted: false,
1726
+ };
1727
+ journal.push(entry);
1728
+ try {
1729
+ renameSync(file.wrapperPath, entry.stagedWrapperPath);
1730
+ const stagedWrapper = stableShimPathProbe(entry.stagedWrapperPath);
1731
+ if (!stagedWrapper
1732
+ || !sameFingerprintAfterRename(stagedWrapper.fingerprint, entry.priorWrapperFingerprint)) {
1733
+ throw new Error("Codex autostart shim upgrade could not fingerprint the staged wrapper");
1734
+ }
1735
+ entry.wrapperWriteStarted = true;
1736
+ const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
1737
+ const writtenWrapper = stableShimPathProbe(file.wrapperPath);
1738
+ if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) {
1739
+ throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper");
1740
+ }
1741
+ // Identity is the inode we created; a replacement that landed since the
1742
+ // rename leaves this unset so rollback treats the file as someone else's.
1743
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(file.wrapperPath, writtenInode);
1744
+ } catch (error) {
1745
+ applyError = error instanceof Error ? error : new Error(String(error));
1746
+ break;
1747
+ }
1748
+ }
1749
+
1750
+ let unsafe: UnixShimProbeResult = null;
1751
+ let probeError: Error | null = null;
1752
+ if (!applyError) {
1753
+ try {
1754
+ unsafe = probeUnixShimFiles(candidates);
1755
+ } catch (error) {
1756
+ probeError = error instanceof Error ? error : new Error(String(error));
1757
+ }
1758
+ }
1759
+ const changedDuringProbe = !applyError && !probeError && journal.some(entry => {
1760
+ const wrapper = stableShimPathProbe(entry.file.wrapperPath);
1761
+ const backing = stableShimPathProbe(entry.file.backupPath);
1762
+ return !wrapper || !entry.writtenWrapperFingerprint
1763
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
1764
+ || !backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint);
1765
+ });
1766
+
1767
+ if (applyError || probeError || changedDuringProbe) {
1768
+ const rollbackErrors = rollbackObsoleteUnixShimRefresh(journal);
1769
+ if (applyError || probeError || rollbackErrors.length > 0) {
1770
+ throw new AggregateError(
1771
+ [...(applyError ? [applyError] : []), ...(probeError ? [probeError] : []), ...rollbackErrors],
1772
+ "Codex autostart shim upgrade failed",
1773
+ );
1774
+ }
1775
+ return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
1776
+ }
1777
+
1778
+ if (unsafe) {
1779
+ const cleanupErrors: Error[] = [];
1780
+ for (const entry of [...journal].reverse()) {
1781
+ try {
1782
+ const wrapper = stableShimPathProbe(entry.file.wrapperPath);
1783
+ if (!wrapper || !entry.writtenWrapperFingerprint
1784
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)) {
1785
+ throw new Error("Codex autostart shim upgrade lost wrapper ownership before removal");
1786
+ }
1787
+ const backing = stableShimPathProbe(entry.file.backupPath);
1788
+ if (!backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint)) {
1789
+ throw new Error("Codex autostart shim upgrade backing launcher changed before restoration");
1790
+ }
1791
+ unlinkSync(entry.file.wrapperPath);
1792
+ renameSync(entry.file.backupPath, entry.file.originalPath);
1793
+ if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
1794
+ } catch (error) {
1795
+ cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
1796
+ }
1797
+ }
1798
+ if (cleanupErrors.length > 0) {
1799
+ throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade safety removal failed");
1800
+ }
1801
+ if (existsSync(statePath())) unlinkSync(statePath());
1802
+ return {
1803
+ installed: false,
1804
+ deferred: false,
1805
+ message: "Removed an obsolete Codex autostart shim because its saved launcher failed current validation. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.",
1806
+ };
1807
+ }
1808
+
1809
+ const cleanupErrors: Error[] = [];
1810
+ for (const entry of journal) {
1811
+ try {
1812
+ if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
1813
+ } catch (error) {
1814
+ cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
1815
+ }
1816
+ }
1817
+ if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade cleanup failed");
1818
+ return {
1819
+ installed: true,
1820
+ message: `Upgraded Codex autostart shim at ${candidates.map(file => file.wrapperPath).join(", ")} and validated the saved launcher.`,
1821
+ };
1822
+ }
1823
+
967
1824
  function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
968
1825
  const existing = readState();
969
1826
  if (existing) {
970
1827
  const files = stateFiles(existing);
1828
+ if (!options.expectedReplacements && process.platform !== "win32") {
1829
+ const hasObsoleteShim = files.some(file => {
1830
+ if (file.preserveOnly) return false;
1831
+ const probe = stableShimPathProbe(file.wrapperPath);
1832
+ return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
1833
+ });
1834
+ if (hasObsoleteShim) return refreshObsoleteUnixShims(files);
1835
+ }
971
1836
  if (options.expectedReplacements) {
972
1837
  const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
973
1838
  if (!operations || operations.length === 0) {
@@ -1034,9 +1899,107 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
1034
1899
  for (const target of targets) {
1035
1900
  if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
1036
1901
  }
1902
+ const freshJournal: FreshShimInstallJournalEntry[] = [];
1903
+ let freshApplyError: Error | null = null;
1037
1904
  for (const target of targets) {
1038
- if (existsSync(target.originalPath)) renameSync(target.originalPath, target.backupPath);
1039
- if (!target.preserveOnly) writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
1905
+ const entry: FreshShimInstallJournalEntry = {
1906
+ target,
1907
+ originalMovedToBackup: false,
1908
+ wrapperWriteStarted: false,
1909
+ };
1910
+ freshJournal.push(entry);
1911
+ try {
1912
+ if (existsSync(target.originalPath)) {
1913
+ renameSync(target.originalPath, target.backupPath);
1914
+ entry.originalMovedToBackup = true;
1915
+ // Metadata-only, and before the content probe: an empty or otherwise
1916
+ // unprobeable launcher must still be restorable during rollback.
1917
+ //
1918
+ // Only Unix reaches the rollback path (Windows rethrows freshApplyError
1919
+ // without rolling back), so only Unix may treat a missing fingerprint as
1920
+ // fatal. Throwing here on Windows would abort AFTER the original moved,
1921
+ // stranding the launcher at its backup path with nothing to restore it.
1922
+ const movedOriginalFingerprint = shimPathFingerprint(target.backupPath);
1923
+ if (movedOriginalFingerprint) entry.movedOriginalFingerprint = movedOriginalFingerprint;
1924
+ if (process.platform !== "win32") {
1925
+ if (!movedOriginalFingerprint) {
1926
+ throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
1927
+ }
1928
+ const movedOriginal = stableShimPathProbe(target.backupPath);
1929
+ // A content probe still runs where it can, purely as a consistency
1930
+ // check: disagreement means the file moved under us mid-install.
1931
+ if (movedOriginal && !sameFingerprint(movedOriginal.fingerprint, movedOriginalFingerprint)) {
1932
+ throw new Error("Codex shim fresh install staged launcher changed while being fingerprinted");
1933
+ }
1934
+ }
1935
+ }
1936
+ if (!target.preserveOnly) {
1937
+ entry.wrapperWriteStarted = true;
1938
+ const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
1939
+ entry.writtenWrapperInode = writtenInode;
1940
+ codexShimFreshWriteHookForTests?.();
1941
+ if (process.platform !== "win32") {
1942
+ if (!writtenInode) throw new Error("Codex shim fresh install could not fingerprint the generated wrapper");
1943
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(target.wrapperPath, writtenInode);
1944
+ }
1945
+ }
1946
+ } catch (error) {
1947
+ freshApplyError = error instanceof Error ? error : new Error(String(error));
1948
+ break;
1949
+ }
1950
+ }
1951
+ if (process.platform !== "win32") {
1952
+ if (freshApplyError) {
1953
+ try {
1954
+ rollbackFreshShimInstall(freshJournal);
1955
+ } catch (rollbackError) {
1956
+ throw new AggregateError([freshApplyError, rollbackError], "Codex shim installation and rollback failed");
1957
+ }
1958
+ throw freshApplyError;
1959
+ }
1960
+ let unsafe: UnixShimProbeResult = null;
1961
+ let probeError: Error | null = null;
1962
+ try {
1963
+ unsafe = probeUnixShimFiles(targets);
1964
+ } catch (error) {
1965
+ probeError = error instanceof Error ? error : new Error(String(error));
1966
+ }
1967
+ if (probeError) {
1968
+ try {
1969
+ rollbackFreshShimInstall(freshJournal);
1970
+ } catch (rollbackError) {
1971
+ throw new AggregateError([probeError, rollbackError], "Codex shim probe and install rollback failed");
1972
+ }
1973
+ throw probeError;
1974
+ }
1975
+ const wrapperChangedDuringProbe = freshJournal.some(entry => {
1976
+ if (entry.target.preserveOnly) return false;
1977
+ const wrapper = stableShimPathProbe(entry.target.wrapperPath);
1978
+ return !wrapper || !entry.writtenWrapperFingerprint
1979
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
1980
+ });
1981
+ if (unsafe || wrapperChangedDuringProbe) {
1982
+ rollbackFreshShimInstall(freshJournal);
1983
+ const reason = wrapperChangedDuringProbe
1984
+ ? "the generated wrapper changed during its validation probe"
1985
+ : unsafe === "recursive"
1986
+ ? "the saved launcher resolved back to the generated shim"
1987
+ : unsafe === "timeout"
1988
+ ? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
1989
+ : unsafe === "descendants"
1990
+ ? "the saved launcher left background descendants running after --version"
1991
+ : unsafe === "cleanup"
1992
+ ? "the saved launcher's probe process group could not be terminated cleanly"
1993
+ : "the saved launcher failed its --version probe";
1994
+ return {
1995
+ installed: false,
1996
+ message: wrapperChangedDuringProbe
1997
+ ? `Refusing Codex autostart shim because ${reason}. The concurrent launcher was preserved, and your previous launcher was kept alongside it as \`<codex>.opencodex-real\`; retry after the Codex update finishes, and remove that backup once you are satisfied the launcher on PATH is the one you want.`
1998
+ : `Refusing Codex autostart shim because ${reason}. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.`,
1999
+ };
2000
+ }
2001
+ } else if (freshApplyError) {
2002
+ throw freshApplyError;
1040
2003
  }
1041
2004
  writeState(primaryState(targets));
1042
2005
  return {
@@ -1069,6 +2032,7 @@ export function autoRestoreCodexShim(options: {
1069
2032
 
1070
2033
  const files = stateFiles(state);
1071
2034
  const replacementProbes = new Map<string, StableShimPathProbe>();
2035
+ const obsoleteShimProbes = new Map<string, StableShimPathProbe>();
1072
2036
  const seen = new Set<string>();
1073
2037
  let healthyCount = 0;
1074
2038
  for (const file of files) {
@@ -1083,15 +2047,19 @@ export function autoRestoreCodexShim(options: {
1083
2047
  if (!probe) return { status: "deferred" };
1084
2048
  if (probe.prefix.includes(SHIM_MARKER)) {
1085
2049
  if (!isHealthyShimProbe(probe, state.platform)) return { status: "ineligible" };
2050
+ if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
2051
+ obsoleteShimProbes.set(file.wrapperPath, probe);
2052
+ continue;
2053
+ }
1086
2054
  healthyCount += 1;
1087
2055
  continue;
1088
2056
  }
1089
2057
  replacementProbes.set(file.wrapperPath, probe);
1090
2058
  }
1091
2059
 
1092
- if (replacementProbes.size === 0) return { status: "healthy" };
2060
+ if (replacementProbes.size === 0 && obsoleteShimProbes.size === 0) return { status: "healthy" };
1093
2061
  if (!options.enabled()) return { status: "disabled" };
1094
- if (files.length > 1 && healthyCount > 0) {
2062
+ if (files.length > 1 && (healthyCount > 0 || (replacementProbes.size > 0 && obsoleteShimProbes.size > 0))) {
1095
2063
  return {
1096
2064
  status: "deferred",
1097
2065
  message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
@@ -1103,6 +2071,19 @@ export function autoRestoreCodexShim(options: {
1103
2071
  try {
1104
2072
  options.afterRestoreLockAcquired?.();
1105
2073
  (options.stabilitySleep ?? Bun.sleepSync)(CODEX_SHIM_REPLACEMENT_STABLE_MS);
2074
+ if (obsoleteShimProbes.size > 0) {
2075
+ for (const [path, firstProbe] of obsoleteShimProbes) {
2076
+ const secondProbe = stableShimPathProbe(path);
2077
+ if (!secondProbe || isCurrentUnixShimProbe(secondProbe)
2078
+ || !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
2079
+ }
2080
+ const result = refreshObsoleteUnixShims(files);
2081
+ return result.installed
2082
+ ? { status: "restored", message: result.message }
2083
+ : result.deferred
2084
+ ? { status: "deferred", message: result.message }
2085
+ : { status: "ineligible", message: result.message };
2086
+ }
1106
2087
  const expectedReplacements = new Map<string, ShimPathFingerprint>();
1107
2088
  for (const [path, firstProbe] of replacementProbes) {
1108
2089
  const secondProbe = stableShimPathProbe(path);