@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,165 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { dlopen, ptr, toArrayBuffer, type Pointer } from "bun:ffi";
4
+ import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsSystemDirectory } from "../../lib/windows-elevation";
5
+
6
+ const MAX_INPUT = 65_536;
7
+ const MAX_OUTPUT = 131_072;
8
+ const PURPOSE = "opencodex/codex-desktop-compatibility/authority/v1";
9
+ const ENTROPY = new TextEncoder().encode(PURPOSE);
10
+ // Only the PowerShell fallback is bounded by a clock. A cold Windows PowerShell that must also load
11
+ // System.Security has been measured past 10s on a loaded machine (Windows CI run 38591626343).
12
+ const DPAPI_TIMEOUT_MS = 20_000;
13
+ const SCRIPT = [
14
+ "$ErrorActionPreference='Stop'",
15
+ "Add-Type -AssemblyName System.Security",
16
+ "$r=[Console]::In.ReadToEnd()|ConvertFrom-Json",
17
+ "$data=[Convert]::FromBase64String($r.data)",
18
+ `if($data.Length -eq 0 -or $data.Length -gt ${MAX_INPUT}){throw 'Invalid data size'}`,
19
+ `$entropy=[Text.Encoding]::UTF8.GetBytes('${PURPOSE}')`,
20
+ "$scope=[Security.Cryptography.DataProtectionScope]::CurrentUser",
21
+ "if($r.operation -eq 'protect'){",
22
+ " $out=[Security.Cryptography.ProtectedData]::Protect($data,$entropy,$scope)",
23
+ "}elseif($r.operation -eq 'unprotect'){",
24
+ " $out=[Security.Cryptography.ProtectedData]::Unprotect($data,$entropy,$scope)",
25
+ "}else{throw 'Invalid operation'}",
26
+ "[Console]::Out.Write([Convert]::ToBase64String($out))",
27
+ ].join("\n");
28
+
29
+ type Operation = "protect" | "unprotect";
30
+
31
+ export interface AuthorityKeyProtection {
32
+ protect(cleartext: Uint8Array): Promise<Uint8Array>;
33
+ unprotect(ciphertext: Uint8Array): Promise<Uint8Array>;
34
+ }
35
+
36
+ // CRYPTPROTECT_UI_FORBIDDEN and no CRYPTPROTECT_LOCAL_MACHINE: the CurrentUser scope that
37
+ // ProtectedData.Protect(..., CurrentUser) requests, so blobs written by either path read in the other.
38
+ const CRYPTPROTECT_UI_FORBIDDEN = 0x1;
39
+ // DATA_BLOB on x64: DWORD cbData, four bytes of padding, BYTE* pbData.
40
+ const DATA_BLOB_BYTES = 16;
41
+ const DPAPI_SIGNATURE = { args: ["ptr", "ptr", "ptr", "ptr", "ptr", "u32", "ptr"], returns: "i32" } as const;
42
+
43
+ type NativeDpapi = (operation: Operation, data: Uint8Array) => Uint8Array;
44
+ let nativeDpapi: NativeDpapi | undefined;
45
+
46
+ function dataBlob(bytes: Uint8Array): Buffer {
47
+ const blob = Buffer.alloc(DATA_BLOB_BYTES);
48
+ blob.writeUInt32LE(bytes.byteLength, 0);
49
+ blob.writeBigUInt64LE(BigInt(ptr(bytes)), 8);
50
+ return blob;
51
+ }
52
+
53
+ /**
54
+ * In-process CryptProtectData/CryptUnprotectData. Spawning Windows PowerShell for every call cost
55
+ * seconds of cold start (past 20s on a loaded CI runner) for a call that takes milliseconds.
56
+ * crypt32 is loaded from the GetSystemDirectoryW path so the DLL search order cannot substitute it.
57
+ * Returns null where Bun's FFI is unavailable (Windows ARM64), leaving the PowerShell helper.
58
+ */
59
+ function loadNativeDpapi(): NativeDpapi | null {
60
+ if (nativeDpapi) return nativeDpapi;
61
+ if (process.platform !== "win32" || process.arch !== "x64") return null;
62
+ let crypt32;
63
+ let kernel32;
64
+ try {
65
+ const systemDirectory = resolveTrustedWindowsSystemDirectory();
66
+ crypt32 = dlopen(join(systemDirectory, "crypt32.dll"), {
67
+ CryptProtectData: DPAPI_SIGNATURE,
68
+ CryptUnprotectData: DPAPI_SIGNATURE,
69
+ }).symbols;
70
+ kernel32 = dlopen(join(systemDirectory, "kernel32.dll"), {
71
+ LocalFree: { args: ["ptr"], returns: "ptr" },
72
+ // Wipes by address, so clearing never depends on a JS view having been created.
73
+ RtlZeroMemory: { args: ["ptr", "u64"], returns: "void" },
74
+ }).symbols;
75
+ } catch {
76
+ return null;
77
+ }
78
+ const { CryptProtectData, CryptUnprotectData } = crypt32;
79
+ const { LocalFree, RtlZeroMemory } = kernel32;
80
+ nativeDpapi = (operation, data) => {
81
+ const input = dataBlob(data);
82
+ const entropy = dataBlob(ENTROPY);
83
+ const output = Buffer.alloc(DATA_BLOB_BYTES);
84
+ const call = operation === "protect" ? CryptProtectData : CryptUnprotectData;
85
+ // Description, reserved and prompt are null; Windows reports no detail worth propagating.
86
+ if (!call(ptr(input), null, ptr(entropy), null, null, CRYPTPROTECT_UI_FORBIDDEN, ptr(output))) {
87
+ throw new Error("desktop_compatibility_key_protection_failed");
88
+ }
89
+ const length = output.readUInt32LE(0);
90
+ const address = Number(output.readBigUInt64LE(8)) as Pointer;
91
+ // Every step after a successful call sits inside try so the native buffer is always
92
+ // released, even if creating the view over it throws.
93
+ let view: Uint8Array | null = null;
94
+ try {
95
+ view = length > 0 ? new Uint8Array(toArrayBuffer(address, 0, length)) : null;
96
+ if (length === 0 || length > MAX_INPUT) throw new Error("desktop_compatibility_key_size");
97
+ return Uint8Array.from(view!);
98
+ } finally {
99
+ // The unprotected buffer is cleartext key material; wipe it natively before LocalFree
100
+ // releases it, including when the size check or view creation fails. LocalFree still
101
+ // runs if wiping throws.
102
+ try {
103
+ if (operation === "unprotect" && length > 0) RtlZeroMemory(address, length);
104
+ } finally {
105
+ LocalFree(address);
106
+ }
107
+ }
108
+ };
109
+ return nativeDpapi;
110
+ }
111
+
112
+ /** The payload travels on stdin, never in process arguments, environment or logs. */
113
+ function runPowerShellDpapi(operation: Operation, data: Uint8Array, timeoutMs = DPAPI_TIMEOUT_MS): Promise<Uint8Array> {
114
+ return new Promise((resolve, reject) => {
115
+ const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
116
+ timeout: timeoutMs, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
117
+ }, (error, stdout) => {
118
+ // Do not include PowerShell stderr, command output or the input in a propagated error.
119
+ if (error || !/^[A-Za-z0-9+/]+={0,2}$/.test(stdout.trim())) {
120
+ reject(new Error("desktop_compatibility_key_protection_failed")); return;
121
+ }
122
+ const bytes = Buffer.from(stdout.trim(), "base64");
123
+ if (bytes.length === 0 || bytes.length > MAX_INPUT) {
124
+ reject(new Error("desktop_compatibility_key_size")); return;
125
+ }
126
+ resolve(bytes);
127
+ });
128
+ child.stdin?.on("error", () => { /* execFile's completion reports the failed child. */ });
129
+ child.stdin?.end(JSON.stringify({ operation, data: Buffer.from(data).toString("base64") }));
130
+ });
131
+ }
132
+
133
+ function validated(data: Uint8Array): Uint8Array {
134
+ if (process.platform !== "win32") throw new Error("desktop_compatibility_windows_required");
135
+ if (data.byteLength === 0 || data.byteLength > MAX_INPUT) throw new Error("desktop_compatibility_key_size");
136
+ return data;
137
+ }
138
+
139
+ async function runDpapi(operation: Operation, data: Uint8Array): Promise<Uint8Array> {
140
+ validated(data);
141
+ const native = loadNativeDpapi();
142
+ return native ? native(operation, data) : runPowerShellDpapi(operation, data);
143
+ }
144
+
145
+ /** CurrentUser protects against other OS identities, not other processes of the same user. */
146
+ export const windowsAuthorityKeyProtection: AuthorityKeyProtection = {
147
+ protect: bytes => runDpapi("protect", bytes),
148
+ unprotect: bytes => runDpapi("unprotect", bytes),
149
+ };
150
+
151
+ /**
152
+ * Test-only: the PowerShell helper (Windows ARM64, and every key stored before the native path).
153
+ * The caller owns the bound because a compatibility check must not depend on cold-start latency.
154
+ */
155
+ export function powerShellAuthorityKeyProtectionForTests(timeoutMs: number): AuthorityKeyProtection {
156
+ return {
157
+ protect: async bytes => runPowerShellDpapi("protect", validated(bytes), timeoutMs),
158
+ unprotect: async bytes => runPowerShellDpapi("unprotect", validated(bytes), timeoutMs),
159
+ };
160
+ }
161
+
162
+ /** Test-only: whether this process protects keys in-process rather than through PowerShell. */
163
+ export function usesNativeDpapiForTests(): boolean {
164
+ return process.platform === "win32" && loadNativeDpapi() !== null;
165
+ }
@@ -0,0 +1,86 @@
1
+ import type { DesktopAppInstall, DesktopExec, DesktopProcess } from "../desktop-app/types";
2
+ import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
3
+ import { WINDOWS_ACTIVATION_SOURCE } from "./windows-activation-source";
4
+ import { readDesktopCompatibilityLaunch } from "./runtime-ownership";
5
+
6
+ /** Only the feature's loopback PAC endpoint may become a launch argument. */
7
+ export function validatedCompatibilityPacUrl(value: string): string {
8
+ let url: URL;
9
+ try { url = new URL(value); } catch { throw new Error("desktop_compatibility_invalid_pac_url"); }
10
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || Number(url.port) < 1
11
+ || url.username || url.password || url.search || url.hash
12
+ || !/^\/[a-zA-Z0-9-]{16,80}\/proxy\.pac$/.test(url.pathname)
13
+ || value !== url.href) throw new Error("desktop_compatibility_invalid_pac_url");
14
+ return url.href;
15
+ }
16
+
17
+ function literal(value: string): string { return `'${value.replaceAll("'", "''")}'`; }
18
+
19
+ export function compatibilityActivationScript(install: DesktopAppInstall, pacUrl: string): string {
20
+ const pac = validatedCompatibilityPacUrl(pacUrl);
21
+ if (!/^OpenAI\.Codex(?:Beta)?_[A-Za-z0-9]+$/.test(install.id) || install.relaunch !== `${install.id}!App`) {
22
+ throw new Error("desktop_compatibility_invalid_package");
23
+ }
24
+ return [
25
+ "$ErrorActionPreference='Stop'",
26
+ `$family=${literal(install.id)}; $root=${literal(install.root)}; $pac=${literal(pac)}`,
27
+ `$p=Get-AppxPackage -Name ${literal(install.id.split("_")[0]!)}`,
28
+ "$p=@($p|Where-Object {$_.PackageFamilyName -eq $family -and $_.InstallLocation -ieq $root})",
29
+ "if($p.Count -ne 1){throw 'Package changed'}; $p=$p[0]",
30
+ "$manifest=Get-AppxPackageManifest -Package $p.PackageFullName",
31
+ "$entry=@($manifest.Package.Applications.Application|Where-Object {$_.Id -eq 'App' -and $_.Executable.Replace('/','\\') -ieq 'app\\ChatGPT.exe'})",
32
+ "if($entry.Count -ne 1){throw 'Application entry changed'}",
33
+ "Add-Type -TypeDefinition @'", WINDOWS_ACTIVATION_SOURCE, "'@",
34
+ "$pidValue=[OpenCodexPackageActivation]::Activate($family+'!App','--proxy-pac-url='+$pac)",
35
+ "if([OpenCodexPackageActivation]::PackageOf($pidValue) -ne $p.PackageFullName){throw 'Package identity missing'}",
36
+ "$running=Get-CimInstance Win32_Process -Filter ('ProcessId='+$pidValue)",
37
+ "$expected=Join-Path $p.InstallLocation 'app\\ChatGPT.exe'",
38
+ "if(-not $running -or $running.ExecutablePath -ine $expected -or $running.CommandLine -notmatch ('(?:^|[\\s\"])--proxy-pac-url='+[regex]::Escape($pac)+'(?:$|[\\s\"])')){throw 'Launch arguments not observed'}",
39
+ "[pscustomobject]@{pid=[int]$pidValue;packageFullName=$p.PackageFullName;verified=$true}|ConvertTo-Json -Compress",
40
+ ].join("\n");
41
+ }
42
+
43
+
44
+ /** Preserve only the current runtime's PAC and lifetime, never trust a URL shape alone. */
45
+ export function captureWindowsCompatibilityContext(processes: readonly DesktopProcess[]): Record<string, string> {
46
+ const members = new Set(processes.map(entry => entry.pid));
47
+ const urls = new Set<string>();
48
+ for (const entry of processes.filter(value => !members.has(value.parentPid))) {
49
+ if (entry.commandLine !== undefined && entry.commandLine.trim() === "") {
50
+ throw new Error("desktop_compatibility_launch_context_unavailable");
51
+ }
52
+ for (const match of (entry.commandLine ?? "").matchAll(/(?:^|\s)"?--proxy-pac-url=([^"\s]+)"?/g)) {
53
+ const url = match[1]!;
54
+ if (url.startsWith("http://127.0.0.1:")) urls.add(validatedCompatibilityPacUrl(url));
55
+ }
56
+ }
57
+ if (urls.size > 1) throw new Error("desktop_compatibility_conflicting_launch_context");
58
+ if (urls.size === 0) return {};
59
+ const pacUrl = [...urls][0]!, owner = readDesktopCompatibilityLaunch();
60
+ if (!owner || owner.pacUrl !== pacUrl) throw new Error("desktop_compatibility_launch_owner_unverified");
61
+ return { codexCompatibilityPacUrl: pacUrl, codexCompatibilityGeneration: owner.generation };
62
+ }
63
+
64
+ /** Recheck after the asynchronous stop ladder: identical endpoints can belong to a new runtime. */
65
+ export function assertWindowsCompatibilityContext(context: Record<string, string>): void {
66
+ const owner = readDesktopCompatibilityLaunch();
67
+ if (!owner || owner.pacUrl !== context.codexCompatibilityPacUrl || owner.generation !== context.codexCompatibilityGeneration) {
68
+ throw new Error("desktop_compatibility_launch_owner_unverified");
69
+ }
70
+ }
71
+
72
+ export function activateWindowsCodexCompatibility(exec: DesktopExec, install: DesktopAppInstall, pacUrl: string): { pid: number; packageFullName: string } {
73
+ const script = compatibilityActivationScript(install, pacUrl);
74
+ let text: string;
75
+ try { text = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 15_000, windowsHide: true }); }
76
+ catch { throw new Error("desktop_compatibility_activation_unverified"); }
77
+ let result: unknown;
78
+ try { result = JSON.parse(text.trim().split(/\r?\n/).at(-1) ?? ""); }
79
+ catch { throw new Error("desktop_compatibility_activation_unverified"); }
80
+ if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("desktop_compatibility_activation_unverified");
81
+ const value = result as Record<string, unknown>;
82
+ if (value.verified !== true || !Number.isSafeInteger(value.pid) || Number(value.pid) <= 0
83
+ || typeof value.packageFullName !== "string" || !value.packageFullName.startsWith(install.id.split("_")[0]! + "_")
84
+ || !value.packageFullName.endsWith("_" + install.id.split("_")[1]!)) throw new Error("desktop_compatibility_activation_unverified");
85
+ return { pid: Number(value.pid), packageFullName: value.packageFullName };
86
+ }
@@ -0,0 +1,28 @@
1
+ import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
2
+ import type { DesktopExec } from "../desktop-app/types";
3
+ import { activateWindowsCodexCompatibility, validatedCompatibilityPacUrl } from "./windows-package-command";
4
+ export { compatibilityActivationScript, validatedCompatibilityPacUrl } from "./windows-package-command";
5
+
6
+ export interface CompatibilityLaunchIo {
7
+ exec?: DesktopExec;
8
+ platform?: string;
9
+ }
10
+ export type CompatibilityLaunchResult =
11
+ | { status: "started"; pid: number; packageFullName: string }
12
+ | { status: "refused"; reason: "unsupported_platform" | "test_environment" | "package_unavailable" | "app_running" | "process_probe_failed" | "activation_failed" };
13
+
14
+ /** Never quits an app, launches the raw EXE, changes login or installs a watcher. */
15
+ export function launchWindowsCodexCompatibility(pacUrl: string, io: CompatibilityLaunchIo = {}): CompatibilityLaunchResult {
16
+ validatedCompatibilityPacUrl(pacUrl);
17
+ if ((io.platform ?? process.platform) !== "win32") return { status: "refused", reason: "unsupported_platform" };
18
+ if (process.env.OCX_TEST_HOME_GUARD === "1" && !io.exec) return { status: "refused", reason: "test_environment" };
19
+ const exec = io.exec ?? windowsDefaultExec;
20
+ const install = windowsDesktopAppAdapter.discover(exec);
21
+ if (!install) return { status: "refused", reason: "package_unavailable" };
22
+ const processes = windowsDesktopAppAdapter.listProcesses(exec, install);
23
+ if (processes === null) return { status: "refused", reason: "process_probe_failed" };
24
+ if (processes.length > 0) return { status: "refused", reason: "app_running" };
25
+ try {
26
+ return { status: "started", ...activateWindowsCodexCompatibility(exec, install, pacUrl) };
27
+ } catch { return { status: "refused", reason: "activation_failed" }; }
28
+ }
@@ -44,6 +44,10 @@ import { resolveCodexHomeDir } from "./home";
44
44
  import { resolveAndPersistCodexRuntime } from "./runtime";
45
45
  import { decodeOverlayState } from "./shim-state-file";
46
46
  import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy";
47
+ import { rootAssignmentKey, sourceText, splitSourceLines } from "./toml-source-lines";
48
+
49
+ const AGENTS_THREAD_LIMIT_KEY = "max_concurrent_threads_per_session";
50
+ const LEGACY_AGENTS_THREAD_LIMIT_KEY = "max_threads";
47
51
 
48
52
  /** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
49
53
  export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input";
@@ -328,36 +332,31 @@ export function isDefaultModeRequestUserInputEnabled(configPath?: string): boole
328
332
  }
329
333
 
330
334
  /**
331
- * TRUE when config.toml still carries `[agents] max_threads` — codex-rs REFUSES to
332
- * boot with that key while multi_agent_v2 is enabled ("agents.max_threads cannot be
333
- * set when features.multi_agent_v2 is enabled", core/src/config/mod.rs:1421). The
334
- * `ocx v2 on` flow warns about it instead of editing config itself.
335
+ * Whether either spelling of the [agents] limit is present, including invalid
336
+ * values. Keep the exported compatibility name; migrations must remove both.
335
337
  */
336
338
  export function hasAgentsMaxThreads(configPath?: string): boolean {
337
339
  const content = readConfigText(configPath);
338
340
  if (content === null) return false;
339
341
  const parsed = parsedTomlTable(content, "agents");
340
- if (parsed !== null) return Object.hasOwn(parsed, "max_threads");
341
- const agents = tomlTableBody(content, "agents");
342
- if (agents === null) return false;
343
- return /^\s*max_threads\s*=/m.test(agents);
342
+ if (parsed !== null) return Object.hasOwn(parsed, AGENTS_THREAD_LIMIT_KEY) || Object.hasOwn(parsed, LEGACY_AGENTS_THREAD_LIMIT_KEY);
343
+ return agentsThreadLimitEntries(content).entries.length > 0;
344
344
  }
345
345
 
346
- /** Current legacy v1 `[agents] max_threads`, or null when absent/invalid. */
346
+ /** Canonical [agents] limit, falling back to max_threads only when absent. */
347
347
  export function getAgentsMaxThreads(configPath?: string): number | null {
348
348
  const content = readConfigText(configPath);
349
349
  if (content === null) return null;
350
350
  const parsed = parsedTomlTable(content, "agents");
351
351
  if (parsed !== null) {
352
- const value = parsed.max_threads;
352
+ const value = Object.hasOwn(parsed, AGENTS_THREAD_LIMIT_KEY) ? parsed[AGENTS_THREAD_LIMIT_KEY] : parsed[LEGACY_AGENTS_THREAD_LIMIT_KEY];
353
353
  return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null;
354
354
  }
355
- const agents = tomlTableBody(content, "agents");
356
- if (agents === null) return null;
357
- const m = agents.match(/^\s*max_threads\s*=\s*(\d+)\s*(?:#.*)?$/m);
358
- if (!m) return null;
359
- const value = Number(m[1]);
360
- return Number.isInteger(value) && value >= 1 ? value : null;
355
+ // Unrelated valid 64-bit integers can make Bun reject the whole document.
356
+ // Decode the same isolated structural assignment the editor uses, including
357
+ // quoted keys and non-decimal integers, rather than treating unreadable as unset.
358
+ const entries = agentsThreadLimitEntries(content).entries;
359
+ return (entries.find(entry => entry.key === AGENTS_THREAD_LIMIT_KEY) ?? entries[0])?.value ?? null;
361
360
  }
362
361
 
363
362
  /**
@@ -1310,33 +1309,59 @@ function editAgentsMaxThreads(value: number | null, configPath?: string, migrate
1310
1309
  const path = configPath ?? activeCodexConfigPath();
1311
1310
  return editCodexConfigToml(path, (content): TomlEditOutcome => {
1312
1311
  const eol = dominantEol(content);
1313
- const lines = content.split(/\r?\n/);
1314
- const headerIdx = lines.findIndex(l => /^\s*\[agents\]\s*(?:#.*)?$/.test(l));
1312
+ const { lines, headerIdx, entries } = agentsThreadLimitEntries(content);
1315
1313
  if (headerIdx === -1) {
1316
1314
  if (value === null) return { next: content };
1317
- const separator = lines.length > 0 && lines[lines.length - 1] !== "" ? [""] : [];
1318
- lines.push(...separator, "[agents]", `max_threads = ${value}${migratedComment ?? ""}`);
1319
- return { next: applyEol(lines.join("\n"), eol) };
1315
+ const separator = content.length > 0 && !content.endsWith("\n") ? eol : "";
1316
+ return { next: `${content}${separator}${eol}[agents]${eol}${AGENTS_THREAD_LIMIT_KEY} = ${value}${migratedComment ?? ""}${eol}` };
1320
1317
  }
1321
- let end = lines.length;
1322
- for (let i = headerIdx + 1; i < lines.length; i++) {
1323
- if (/^\s*\[/.test(lines[i])) { end = i; break; }
1318
+ const selected = entries.find(entry => entry.key === AGENTS_THREAD_LIMIT_KEY) ?? entries[0];
1319
+ if (entries.length === 1 && selected?.key === AGENTS_THREAD_LIMIT_KEY && selected.value === value
1320
+ && value !== null && mergeTrailingComments(selected.comment, migratedComment) === (selected.comment ?? "")) return { next: content };
1321
+ let comment = selected?.comment;
1322
+ for (const entry of entries) comment = mergeTrailingComments(comment, entry.comment);
1323
+ comment = mergeTrailingComments(comment, migratedComment);
1324
+ for (const entry of [...entries].reverse()) {
1325
+ if (value !== null && entry === selected) {
1326
+ const line = lines[entry.index]!;
1327
+ line.text = `${line.text.match(/^\s*/)?.[0] ?? ""}${AGENTS_THREAD_LIMIT_KEY} = ${value}${comment}`;
1328
+ } else lines.splice(entry.index, 1);
1324
1329
  }
1325
- const keyRe = /^(\s*)max_threads\s*=\s*(\d+)(\s*#.*)?$/;
1326
- for (let i = headerIdx + 1; i < end; i++) {
1327
- const m = lines[i].match(keyRe);
1328
- if (!m) continue;
1329
- if (value === null) lines.splice(i, 1);
1330
- else if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { next: content };
1331
- else lines[i] = `${m[1]}max_threads = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
1332
- return { next: applyEol(lines.join("\n"), eol) };
1330
+ if (value !== null && !selected) {
1331
+ if (!lines[headerIdx]!.eol) lines[headerIdx]!.eol = eol;
1332
+ lines.splice(headerIdx + 1, 0, { text: `${AGENTS_THREAD_LIMIT_KEY} = ${value}${comment}`, eol, structural: true });
1333
1333
  }
1334
- if (value === null) return { next: content };
1335
- lines.splice(headerIdx + 1, 0, `max_threads = ${value}${migratedComment ?? ""}`);
1336
- return { next: applyEol(lines.join("\n"), eol) };
1334
+ return { next: sourceText(lines) };
1337
1335
  });
1338
1336
  }
1339
1337
 
1338
+ /** Only structural assignments in the supported agents table are editable. */
1339
+ function agentsThreadLimitEntries(content: string) {
1340
+ const lines = splitSourceLines(content);
1341
+ const headerIdx = lines.findIndex(line => line.structural && /^\s*\[agents\]\s*(?:#.*)?$/.test(line.text));
1342
+ const entries: Array<{ index: number; key: string; value: number | null; comment?: string; editable: boolean }> = [];
1343
+ if (headerIdx !== -1) {
1344
+ for (let index = headerIdx + 1; index < lines.length; index++) {
1345
+ const line = lines[index]!;
1346
+ if (!line.structural) continue;
1347
+ if (/^\s*\[/.test(line.text)) break;
1348
+ const key = rootAssignmentKey(line.text);
1349
+ if (key !== AGENTS_THREAD_LIMIT_KEY && key !== LEGACY_AGENTS_THREAD_LIMIT_KEY) continue;
1350
+ const assignment = findTomlAssignment(line.text, key);
1351
+ const editable = assignment !== null && (index + 1 === lines.length || lines[index + 1]!.structural);
1352
+ let value: unknown;
1353
+ try { value = (Bun.TOML.parse(line.text) as Record<string, unknown>)[key]; } catch {
1354
+ // Match the reader's large-integer fallback without widening edit syntax.
1355
+ const integer = assignment && /^\d+$/.exec(line.text.slice(assignment.valueStart, assignment.valueEnd).trim());
1356
+ if (integer) value = Number(integer[0]);
1357
+ }
1358
+ entries.push({ index, key, editable, value: typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null,
1359
+ comment: assignment ? line.text.slice(assignment.valueEnd).match(/\s*#.*$/)?.[0] : undefined });
1360
+ }
1361
+ }
1362
+ return { lines, headerIdx, entries };
1363
+ }
1364
+
1340
1365
  function removeMaxConcurrentThreads(configPath?: string): ConfigEditResult {
1341
1366
  const path = configPath ?? activeCodexConfigPath();
1342
1367
  return editCodexConfigToml(path, (content): TomlEditOutcome => {
@@ -1439,7 +1464,9 @@ function discoverStoredThreadLimit(configPath?: string): { value: number; units:
1439
1464
  }
1440
1465
 
1441
1466
  function activeThreadComment(content: string, v2Enabled: boolean): string | undefined {
1442
- const legacy = tomlTableBody(content, "agents")?.match(/^\s*max_threads\s*=\s*\d+(\s*#.*)$/m)?.[1];
1467
+ const entries = agentsThreadLimitEntries(content).entries;
1468
+ let legacy = (entries.find(entry => entry.key === AGENTS_THREAD_LIMIT_KEY) ?? entries[0])?.comment;
1469
+ for (const entry of entries) legacy = mergeTrailingComments(legacy, entry.comment) || undefined;
1443
1470
  const dedicated = tomlTableBody(content, "features.multi_agent_v2")
1444
1471
  ?.match(/^\s*max_concurrent_threads_per_session\s*=\s*\d+(\s*#.*)$/m)?.[1];
1445
1472
  const features = tomlTableBody(content, "features");
@@ -1503,8 +1530,8 @@ export type MultiAgentV2TransitionResult =
1503
1530
  | { ok: true; changed: boolean; threadLimit: number | null }
1504
1531
  | { ok: false; error: string; retryable?: boolean };
1505
1532
 
1506
- function transitionConfigError(content: string): string | null {
1507
- if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.max_threads)\s*=/m.test(content)) {
1533
+ function transitionConfigError(content: string, explicitThreadLimit: boolean): string | null {
1534
+ if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.(?:max_threads|max_concurrent_threads_per_session))\s*=/m.test(content)) {
1508
1535
  return "dotted multi-agent config keys are not supported for automatic migration";
1509
1536
  }
1510
1537
  const dedicatedTables = content.match(/^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/gm) ?? [];
@@ -1521,9 +1548,18 @@ function transitionConfigError(content: string): string | null {
1521
1548
  if (features && /^\s*multi_agent_v2\.(?:enabled|max_concurrent_threads_per_session)\s*=/m.test(features)) {
1522
1549
  return "dotted multi_agent_v2 fields are not supported for automatic migration";
1523
1550
  }
1524
- const agents = tomlTableBody(content, "agents");
1525
- if ((agents?.match(/^\s*max_threads\s*=/gm) ?? []).length > 1) {
1526
- return "duplicate agents.max_threads definitions cannot be migrated safely";
1551
+ const entries = agentsThreadLimitEntries(content).entries;
1552
+ const parsedAgents = parsedTomlTable(content, "agents");
1553
+ for (const key of [AGENTS_THREAD_LIMIT_KEY, LEGACY_AGENTS_THREAD_LIMIT_KEY]) {
1554
+ const matches = entries.filter(entry => entry.key === key);
1555
+ if (matches.length > 1) return `duplicate agents.${key} definitions cannot be migrated safely`;
1556
+ if (parsedAgents && Object.hasOwn(parsedAgents, key) && matches.length === 0) {
1557
+ return "unsupported agents thread-limit storage cannot be migrated safely";
1558
+ }
1559
+ }
1560
+ if (entries.some(entry => !entry.editable)) return "unsupported agents thread-limit value cannot be migrated safely";
1561
+ if (!explicitThreadLimit && (entries.some(entry => entry.value === null) || (entries.length === 2 && entries[0]!.value !== entries[1]!.value))) {
1562
+ return "conflicting or invalid agents thread-limit aliases require an explicit thread limit";
1527
1563
  }
1528
1564
  const dedicated = tomlTableBody(content, "features.multi_agent_v2");
1529
1565
  if ((dedicated?.match(/^\s*max_concurrent_threads_per_session\s*=/gm) ?? []).length > 1) {
@@ -1542,15 +1578,15 @@ export function transitionMultiAgentV2(
1542
1578
  toggleFeature: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void,
1543
1579
  options: { configPath?: string; threadLimit?: number; heldConfigWriteLock?: LockHandle } = {},
1544
1580
  ): MultiAgentV2TransitionResult {
1545
- if (options.threadLimit !== undefined && (!Number.isInteger(options.threadLimit) || options.threadLimit < 1)) {
1546
- return { ok: false, error: "thread limit must be an integer >= 1" };
1581
+ if (options.threadLimit !== undefined && (!Number.isSafeInteger(options.threadLimit) || options.threadLimit < 1)) {
1582
+ return { ok: false, error: "thread limit must be a safe integer >= 1" };
1547
1583
  }
1548
1584
  const path = options.configPath ?? activeCodexConfigPath();
1549
1585
  if (codexHomeIsAbsent(dirname(path))) return { ok: false, error: `config.toml not readable at ${path}` };
1550
1586
  const runTransition = (held: LockHandle): MultiAgentV2TransitionResult => {
1551
1587
  const original = readConfigText(path);
1552
1588
  if (original === null) return { ok: false, error: `config.toml not readable at ${path}` };
1553
- const preflightError = transitionConfigError(original);
1589
+ const preflightError = transitionConfigError(original, options.threadLimit !== undefined);
1554
1590
  if (preflightError) return { ok: false, error: preflightError };
1555
1591
  const beforeEnabled = isMultiAgentV2Enabled(path);
1556
1592
  // A caller-supplied limit is already in the DESTINATION backend's units and is
@@ -1573,6 +1609,9 @@ export function transitionMultiAgentV2(
1573
1609
  ? v1ChildLimitToV2TotalLimit(discovered.value)
1574
1610
  : v2TotalLimitToV1ChildLimit(discovered.value);
1575
1611
  }
1612
+ if (threadLimit !== null && !Number.isSafeInteger(threadLimit)) {
1613
+ return { ok: false, error: "stored thread limit cannot be written safely; supply an explicit safe integer limit" };
1614
+ }
1576
1615
  const migratedComment = activeThreadComment(original, beforeEnabled);
1577
1616
  try {
1578
1617
  return withConfigWriteRecovery(path, held, () => {
@@ -1606,7 +1645,8 @@ export function transitionMultiAgentV2(
1606
1645
  return editAgentsMaxThreads(threadLimit, tempPath, migratedComment);
1607
1646
  });
1608
1647
  if (!target.ok) throw new Error(target.error);
1609
- if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit) throw new Error("v1 thread-limit migration postcondition failed");
1648
+ const agents = parsedTomlTable(readConfigText(path) ?? "", "agents");
1649
+ if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit || (agents && Object.hasOwn(agents, LEGACY_AGENTS_THREAD_LIMIT_KEY))) throw new Error("v1 thread-limit migration postcondition failed");
1610
1650
  }
1611
1651
  return { ok: true, changed: readConfigText(path) !== original, threadLimit };
1612
1652
  });
@@ -1831,7 +1831,7 @@ export function countPendingOpencodexHistory(
1831
1831
  : { pendingRows: 0, backupEntries };
1832
1832
  }
1833
1833
  try {
1834
- const db = new Database(stateDbPath, { readonly: true });
1834
+ const db = openCodexStateForPreflight(stateDbPath);
1835
1835
  try {
1836
1836
  db.exec("PRAGMA busy_timeout = 100");
1837
1837
  // Prove the expected history schema is readable without counting unowned routed rows.
@@ -1,7 +1,7 @@
1
1
  import type { AtomicWriteHooks } from "../../config/atomic-write";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { withConfigMutationLockSync } from "../../config";
4
- import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
4
+ import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, removeConfigWriteEntry, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
5
5
  import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "../paths";
6
6
  import { getCodexHome } from "../paths";
7
7
  import { codexHomeIsAbsent } from "../codex-home-owner";
@@ -16,6 +16,9 @@ export function publishCodexArtifact<T>(path: string, held: LockHandle | undefin
16
16
  if (!held) throw new Error("Codex artifact publication requires a held configuration lock");
17
17
  return publishConfigWriteTarget(CODEX_CONFIG_PATH, held, path, run);
18
18
  }
19
+ export function removeCodexProfileEntry(held: LockHandle): void {
20
+ removeConfigWriteEntry(CODEX_CONFIG_PATH, held, CODEX_PROFILE_PATH);
21
+ }
19
22
  /** Courtesy cleanup re-reads provider ownership inside the same config section. */
20
23
  export function cleanExternalProviderJournal(beforeClientWrite?: () => void): void {
21
24
  if (codexHomeIsAbsent(getCodexHome())) return;
@@ -0,0 +1,27 @@
1
+ import { describeHistoryJobFailure, type CodexHistoryJobOutcome } from "../history-job";
2
+
3
+ /**
4
+ * Frame one failed apply history job honestly.
5
+ *
6
+ * A genuine lock keeps the established deferred/SKIPPED wording; any other
7
+ * reason names itself instead of blaming the Codex app/IDE.
8
+ */
9
+ export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legacyMode: boolean): string {
10
+ // A busy database is a deferral no matter which half observed it: the lock
11
+ // contended (blocked/busy), or the worker acquired the lock and then found
12
+ // SQLite busy (failed with a busy history reason). Only those keep the
13
+ // deferred headline; every other failure is a real "NOT changed".
14
+ const busy =
15
+ (outcome.kind === "blocked" && outcome.reason === "busy") ||
16
+ (outcome.kind === "failed" && outcome.historyFailureReason === "busy");
17
+ const partiallyChanged = outcome.kind === "failed"
18
+ && ((outcome.rows ?? 0) > 0 || (outcome.files ?? 0) > 0);
19
+ const headline = partiallyChanged
20
+ ? "Codex resume history changed but did not converge"
21
+ : legacyMode
22
+ ? "Codex resume history sync SKIPPED"
23
+ : busy
24
+ ? "Codex resume history metadata restore deferred"
25
+ : "Codex resume history NOT changed";
26
+ return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
27
+ }
@@ -1,8 +1,8 @@
1
- import { beginCodexWriteSection, publishCodexArtifact } from "./config-write-section";
2
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
1
+ import { beginCodexWriteSection, publishCodexArtifact, removeCodexProfileEntry } from "./config-write-section";
2
+ import { existsSync, lstatSync, readFileSync } from "node:fs";
3
3
  import { normalizeStructuralWhitespace, rootAssignmentKey, rootSourceLines, sourceAssignment, sourceText } from "../toml-source-lines";
4
4
  import { atomicWriteFile } from "../../config";
5
- import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld } from "../config-write-lock";
5
+ import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld, withConfigWriteRecovery } from "../config-write-lock";
6
6
  import type { LockHandle } from "../config-write-lock";
7
7
  import {
8
8
  REALTIME_WS_BASE_URL_KEY,
@@ -206,15 +206,26 @@ export function removeCodexConfig(
206
206
  }
207
207
  const locked = withConfigWriteLockHeld(CODEX_CONFIG_PATH, options.heldConfigWriteLock, held => {
208
208
  beginCodexWriteSection(held);
209
+ if (!options.preserveProfile) {
210
+ try {
211
+ if (lstatSync(CODEX_PROFILE_PATH).isSymbolicLink()) {
212
+ return { success: false, message: "Codex profile is a symbolic link; configuration and recovery evidence were preserved." };
213
+ }
214
+ } catch (error) {
215
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
216
+ }
217
+ }
209
218
  if (!existsSync(CODEX_CONFIG_PATH)) {
210
219
  if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
211
- publishCodexArtifact(CODEX_PROFILE_PATH, held, (path, hooks) => { hooks.validateBeforeRename?.(path); unlinkSync(path); hooks.afterRename?.(path); });
220
+ removeCodexProfileEntry(held);
212
221
  return {
213
222
  success: true,
214
223
  message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
215
224
  };
216
225
  }
217
- const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
226
+ const preimage = readFileSync(CODEX_CONFIG_PATH);
227
+ return withConfigWriteRecovery(CODEX_CONFIG_PATH, held, () => {
228
+ const rawContent = preimage.toString("utf-8");
218
229
  // Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
219
230
  // The unchanged fast path compares in LF space so an untouched file is never rewritten.
220
231
  const eol = dominantEol(rawContent);
@@ -248,7 +259,7 @@ export function removeCodexConfig(
248
259
  publishCodexArtifact(CODEX_CONFIG_PATH, held, (path, hooks) => atomicWriteFile(path, applyEol(finalContent, eol), undefined, hooks));
249
260
  }
250
261
  if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
251
- publishCodexArtifact(CODEX_PROFILE_PATH, held, (path, hooks) => { hooks.validateBeforeRename?.(path); unlinkSync(path); hooks.afterRename?.(path); });
262
+ removeCodexProfileEntry(held);
252
263
  const retainedNote = retainedBlock === null
253
264
  ? ""
254
265
  : " Kept [model_providers.opencodex] so conversations already tagged opencodex still open;"
@@ -272,6 +283,7 @@ export function removeCodexConfig(
272
283
  message: removedMessage,
273
284
  ...(retainedBlock === null ? {} : { retainedProviderTable: retainedBlock.replace(/\n+$/, "").split("\n") }),
274
285
  };
286
+ }, { preimage, requireMatchingPreimage: true });
275
287
  });
276
288
  if (!locked.ok) return { success: false, retryable: locked.error === "locked", message: configWriteLockFailureMessage(locked) };
277
289
  return locked.value;