@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -206,6 +206,27 @@ export function listCodexDesktopAppPids(io: DesktopAppRestartIo = {}): number[]
206
206
  return processes === null ? null : processes.map(entry => entry.pid);
207
207
  }
208
208
 
209
+ /**
210
+ * Pids of the running desktop-app ROOT shells, or null when discovery or the probe failed.
211
+ *
212
+ * Read-only. The shell is the process whose renderer built the model picker, so its start
213
+ * time is what tells `performCodexRestart` whether the picker predates the catalog (#4761).
214
+ * `[]` means the probe ran and the app is not running.
215
+ */
216
+ export function listCodexDesktopShellPids(io: DesktopAppRestartIo = {}): number[] | null {
217
+ const platform = io.platform ?? process.platform;
218
+ const selected = ADAPTERS[platform];
219
+ const adapter = io.adapter ?? selected?.adapter;
220
+ if (!adapter) return null;
221
+ const exec = io.execFile ?? selected?.exec;
222
+ if (!exec) return null;
223
+ const install = adapter.discover(exec);
224
+ if (!install) return null;
225
+ const processes = adapter.listProcesses(exec, install);
226
+ if (processes === null) return null;
227
+ return rootShells(processes, install, adapter).map(entry => entry.pid);
228
+ }
229
+
209
230
  export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopAppRestartResult {
210
231
  const skipped = (reason: DesktopAppRestartReason): DesktopAppRestartResult => ({
211
232
  attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason,
@@ -8,11 +8,21 @@ import type { DesktopAppInstall } from "../desktop-app/types";
8
8
  export const DESKTOP_COMPATIBILITY_ASSESSED_VERSION = "26.924.2738.0";
9
9
  export const DESKTOP_COMPATIBILITY_ASSESSED_FAMILY = "OpenAI.Codex_2p2nqsd0c76g0";
10
10
 
11
- function matchesAssessedPackage(install: DesktopAppInstall): boolean {
11
+ /** Last completed probe outcome. Only public package facts: never paths, keys or account data. */
12
+ export interface DesktopBuildAssessment {
13
+ assessedVersion: string;
14
+ observedVersion: string | null;
15
+ verified: boolean;
16
+ completedAt: number;
17
+ }
18
+
19
+ function assessInstalledPackage(install: DesktopAppInstall): { observedVersion: string | null; verified: boolean } {
12
20
  const name = win32.basename(install.root);
13
21
  const parts = /^OpenAI\.Codex_(\d+\.\d+\.\d+\.\d+)_(x64|arm64|x86|neutral)_([A-Za-z0-9.-]*)_([A-Za-z0-9]+)$/.exec(name);
14
- return install.id === DESKTOP_COMPATIBILITY_ASSESSED_FAMILY && install.relaunch === `${install.id}!App`
15
- && !!parts && parts[1] === DESKTOP_COMPATIBILITY_ASSESSED_VERSION && `OpenAI.Codex_${parts[4]}` === install.id;
22
+ const identified = install.id === DESKTOP_COMPATIBILITY_ASSESSED_FAMILY && install.relaunch === `${install.id}!App`
23
+ && !!parts && `OpenAI.Codex_${parts[4]}` === install.id;
24
+ const observedVersion = identified ? parts![1] : null;
25
+ return { observedVersion, verified: identified && observedVersion === DESKTOP_COMPATIBILITY_ASSESSED_VERSION };
16
26
  }
17
27
 
18
28
  /** Resolve only after the exact asynchronous child has closed, including abort/timeout. */
@@ -34,17 +44,25 @@ function queryInstalledPackage(signal: AbortSignal): Promise<DesktopAppInstall |
34
44
  /** Coalesce concurrent fresh probes; never cache a positive verdict across requests. */
35
45
  export function createInstalledBuildProbe(query: (signal: AbortSignal) => Promise<DesktopAppInstall | null> = queryInstalledPackage) {
36
46
  let pending: Promise<boolean> | null = null, controller: AbortController | null = null, closed = false;
47
+ let lastResult: DesktopBuildAssessment | null = null;
48
+ const record = (install: DesktopAppInstall | null) => {
49
+ const outcome = install ? assessInstalledPackage(install) : { observedVersion: null, verified: false };
50
+ lastResult = { assessedVersion: DESKTOP_COMPATIBILITY_ASSESSED_VERSION, ...outcome, completedAt: Date.now() };
51
+ return outcome.verified;
52
+ };
37
53
  return {
38
54
  check(): Promise<boolean> {
39
55
  if (closed) return Promise.resolve(false);
40
56
  if (pending) return pending;
41
57
  const current = new AbortController(); controller = current;
42
58
  pending = Promise.resolve().then(() => current.signal.aborted ? null : query(current.signal))
43
- .then(install => !closed && !current.signal.aborted && !!install
44
- && matchesAssessedPackage(install))
45
- .catch(() => false).finally(() => { pending = null; controller = null; });
59
+ .then(install => closed || current.signal.aborted ? false : record(install))
60
+ .catch(() => closed || current.signal.aborted ? false : record(null))
61
+ .finally(() => { pending = null; controller = null; });
46
62
  return pending;
47
63
  },
64
+ /** Last completed check only: an aborted or in-flight query never rewrites it. */
65
+ lastResult(): DesktopBuildAssessment | null { return lastResult ? { ...lastResult } : null; },
48
66
  async close(): Promise<void> { closed = true; controller?.abort(); await pending; },
49
67
  };
50
68
  }
@@ -9,7 +9,7 @@ const record = (value: unknown): value is Record<string, unknown> => !!value &&
9
9
  // changes revoke the observed context even if the loopback URL stays the same.
10
10
  const ROUTING_KEYS = ["providers", "defaultProvider", "defaultModelAliases", "customModels", "combos", "routingProfiles",
11
11
  "subagentModelFallback", "subagentModelFallbackByModel", "injectionModel", "compactionRouting", "compactionRecovery",
12
- "blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels"] as const satisfies readonly (keyof OcxConfig)[];
12
+ "blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels", "codexMainAccountHardLockFallback"] as const satisfies readonly (keyof OcxConfig)[];
13
13
  function routingDigest(text: string, owner: NativeCompatibilityOwner): string {
14
14
  const native = Bun.TOML.parse(text) as Record<string, unknown>;
15
15
  // Desktop refreshes MCP endpoints after launch. Tool transport metadata and TOML
@@ -16,7 +16,7 @@ import { launchWindowsCodexCompatibility } from "./windows-package-launch";
16
16
  import { acquireDesktopCompatibilityRuntime, bindDesktopCompatibilityLaunch } from "./runtime-ownership";
17
17
  import { createDesktopConnectionStore, type DesktopConnectionStore } from "./connection-store";
18
18
  import { createNativeRoutingVerifier } from "./routing-preflight";
19
- import { createInstalledBuildProbe } from "./installed-build";
19
+ import { createInstalledBuildProbe, type DesktopBuildAssessment } from "./installed-build";
20
20
 
21
21
  export { DESKTOP_COMPATIBILITY_ASSESSED_VERSION, isAssessedDesktopInstalled } from "./installed-build";
22
22
  export interface DesktopRuntimeIo {
@@ -25,6 +25,8 @@ export interface DesktopRuntimeIo {
25
25
  trust?: (authority: StoredDesktopAuthority) => Promise<"trusted" | "not-trusted" | "unknown">;
26
26
  identity?: ReturnType<typeof createNativeIdentityReader>;
27
27
  buildSupported?: () => boolean | Promise<boolean>;
28
+ /** Internal fixture probe factory; requires testOnly and all sandbox identity/CA seams. */
29
+ testOnlyBuildProbe?: () => ReturnType<typeof createInstalledBuildProbe>;
28
30
  upstreamFetch?: typeof fetch;
29
31
  connectionStore?: DesktopConnectionStore;
30
32
  routingSupported?: () => boolean;
@@ -36,6 +38,7 @@ export interface DesktopRuntimeIo {
36
38
  export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
37
39
  const platform = io.platform ?? process.platform;
38
40
  let buildProbe: ReturnType<typeof createInstalledBuildProbe> | undefined;
41
+ let previousBuildAssessment: DesktopBuildAssessment | null = null;
39
42
  const buildSupported = io.buildSupported ?? (() => buildProbe?.check() ?? Promise.resolve(false));
40
43
  let routingSupported = io.routingSupported ?? (() => false);
41
44
  let contextFailure: "build_unverified" | "native_routing_unverified" | null = null;
@@ -54,6 +57,7 @@ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
54
57
  let pendingCleanup: (() => Promise<void>) | null = null;
55
58
  let releaseOwner: (() => void) | null = null;
56
59
  const status = () => ({ phase, supported: platform === "win32", running: owned !== null, contextFailure,
60
+ ...(buildProbe ? { buildAssessment: buildProbe.lastResult() ?? (previousBuildAssessment ? { ...previousBuildAssessment } : null) } : {}),
57
61
  ...(owned ? { fingerprint: owned.fingerprint, safetyDeadline: owned.deadline, usage: owned.controller.snapshot() } : {}) });
58
62
  async function stop(): Promise<ReturnType<typeof status>> {
59
63
  if (phase === "starting") throw new Error("desktop_compatibility_busy");
@@ -67,11 +71,11 @@ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
67
71
  async function start() {
68
72
  if (phase !== "off") throw new Error("desktop_compatibility_busy");
69
73
  if (platform !== "win32") throw new Error("desktop_compatibility_unsupported");
70
- if (isTestHomeGuardArmed() && !(io.testOnly && io.identity && io.loadAuthority && io.trust && io.buildSupported && io.connectionStore && io.routingSupported)) throw new Error("desktop_compatibility_test_environment");
74
+ if (io.testOnlyBuildProbe && (!io.testOnly || io.buildSupported)) throw new Error("desktop_compatibility_test_environment");
75
+ if ((isTestHomeGuardArmed() || io.testOnlyBuildProbe) && !(io.testOnly && io.identity && io.loadAuthority && io.trust && (io.buildSupported || io.testOnlyBuildProbe) && io.connectionStore && io.routingSupported)) throw new Error("desktop_compatibility_test_environment");
71
76
  if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
72
77
  try { desktopProxyFor(new URL("https://chatgpt.com")); } catch { throw new Error("desktop_compatibility_egress_proxy_invalid"); }
73
78
  releaseOwner = acquireDesktopCompatibilityRuntime(); phase = "starting";
74
- if (!io.buildSupported) buildProbe = createInstalledBuildProbe();
75
79
  let relay: Awaited<ReturnType<typeof startDesktopRelay>> | undefined, proxy: ConnectProxyHandle | undefined;
76
80
  let pac: ReturnType<typeof Bun.serve> | undefined, timer: ReturnType<typeof setInterval> | undefined;
77
81
  let detach: (() => void) | undefined, unbindLaunch: (() => void) | undefined, controller: UsageRelayController | undefined;
@@ -84,6 +88,11 @@ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
84
88
  };
85
89
  pendingCleanup = cleanup;
86
90
  try {
91
+ if (!io.buildSupported) {
92
+ // A fresh probe owns this start; diagnostics retain the last completed observation.
93
+ previousBuildAssessment = buildProbe?.lastResult() ?? previousBuildAssessment;
94
+ buildProbe = io.testOnlyBuildProbe ? io.testOnlyBuildProbe() : createInstalledBuildProbe();
95
+ }
87
96
  if (!io.routingSupported) routingSupported = createNativeRoutingVerifier((await import("../paths")).getCodexHome());
88
97
  if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
89
98
  const directory = join(getConfigDir(), "codex-desktop-compatibility");
@@ -1,12 +1,14 @@
1
1
  import { execFile } from "node:child_process";
2
- import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
2
+ import { join } from "node:path";
3
+ import { dlopen, ptr, toArrayBuffer, type Pointer } from "bun:ffi";
4
+ import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsSystemDirectory } from "../../lib/windows-elevation";
3
5
 
4
6
  const MAX_INPUT = 65_536;
5
7
  const MAX_OUTPUT = 131_072;
6
8
  const PURPOSE = "opencodex/codex-desktop-compatibility/authority/v1";
7
- // A cold Windows PowerShell that must also load System.Security has been measured past 10s on a
8
- // loaded machine (Windows CI run 38591626343). The call is async and off the request path, so a
9
- // wider bound costs nothing while healthy and turns a slow start into success instead of a refusal.
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).
10
12
  const DPAPI_TIMEOUT_MS = 20_000;
11
13
  const SCRIPT = [
12
14
  "$ErrorActionPreference='Stop'",
@@ -24,18 +26,94 @@ const SCRIPT = [
24
26
  "[Console]::Out.Write([Convert]::ToBase64String($out))",
25
27
  ].join("\n");
26
28
 
29
+ type Operation = "protect" | "unprotect";
30
+
27
31
  export interface AuthorityKeyProtection {
28
32
  protect(cleartext: Uint8Array): Promise<Uint8Array>;
29
33
  unprotect(ciphertext: Uint8Array): Promise<Uint8Array>;
30
34
  }
31
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
+
32
112
  /** The payload travels on stdin, never in process arguments, environment or logs. */
33
- function runDpapi(operation: "protect" | "unprotect", data: Uint8Array): Promise<Uint8Array> {
34
- if (process.platform !== "win32") return Promise.reject(new Error("desktop_compatibility_windows_required"));
35
- if (data.byteLength === 0 || data.byteLength > MAX_INPUT) return Promise.reject(new Error("desktop_compatibility_key_size"));
113
+ function runPowerShellDpapi(operation: Operation, data: Uint8Array, timeoutMs = DPAPI_TIMEOUT_MS): Promise<Uint8Array> {
36
114
  return new Promise((resolve, reject) => {
37
- const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
38
- timeout: DPAPI_TIMEOUT_MS, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
115
+ const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
116
+ timeout: timeoutMs, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
39
117
  }, (error, stdout) => {
40
118
  // Do not include PowerShell stderr, command output or the input in a propagated error.
41
119
  if (error || !/^[A-Za-z0-9+/]+={0,2}$/.test(stdout.trim())) {
@@ -52,8 +130,36 @@ function runDpapi(operation: "protect" | "unprotect", data: Uint8Array): Promise
52
130
  });
53
131
  }
54
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
+
55
145
  /** CurrentUser protects against other OS identities, not other processes of the same user. */
56
146
  export const windowsAuthorityKeyProtection: AuthorityKeyProtection = {
57
147
  protect: bytes => runDpapi("protect", bytes),
58
148
  unprotect: bytes => runDpapi("unprotect", bytes),
59
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
+ }
@@ -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
  });
@@ -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,3 +1,4 @@
1
+ import { restoreApprovedCodexProviders } from "../provider-routing";
1
2
  import { beginCodexWriteSection, cleanExternalProviderJournal, publishCodexArtifact } from "./config-write-section";
2
3
  import { assertCodexHomeOwner, codexHomeIsAbsent, codexHomeOwnerBlocksCompensation, CodexHomeOwnerRefusal, type CodexHomeOwnerRefusalReason } from "../codex-home-owner";
3
4
  import { loadConfig } from "../../config";
@@ -115,6 +116,7 @@ export interface CodexRestoreConfigResult {
115
116
  | "owned-fields-stripped"
116
117
  | "routing-restored-provider-retained"
117
118
  | "external-provider-preserved"
119
+ | "approved-provider-restored"
118
120
  | "failed";
119
121
  message: string;
120
122
  retained?: RetainedCodexProviderTable;
@@ -236,6 +238,26 @@ export function failedHistoryRestoreFromOutcome(
236
238
  return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
237
239
  }
238
240
 
241
+ export function finishApprovedProviderRestore(result: CodexNativeRestoreResult, revalidateDesiredState = false): CodexNativeRestoreResult {
242
+ if (!result.success || revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) return result;
243
+ try {
244
+ if (!restoreApprovedCodexProviders()) return result;
245
+ } catch (error) {
246
+ if (error instanceof CodexHomeOwnerRefusal) throw error;
247
+ const message = `Approved Codex provider URL restoration failed: ${error instanceof Error ? error.message : String(error)}`;
248
+ return { ...result, success: false, message: `${result.message} ${message}`, artifacts: {
249
+ ...result.artifacts, config: { ...result.artifacts.config, state: "failed", action: "failed", message },
250
+ } };
251
+ }
252
+ const message = "Approved Codex provider URLs restored; identity, other settings and history preserved.";
253
+ return { ...result, message: result.externalProvider ? message : `${result.message} ${message}`, artifacts: {
254
+ ...result.artifacts,
255
+ config: { ...result.artifacts.config, state: result.artifacts.config.state === "partial" ? "partial" : "ok", changed: true,
256
+ action: result.artifacts.config.changed ? result.artifacts.config.action : "approved-provider-restored",
257
+ message: result.artifacts.config.state === "partial" ? `${result.artifacts.config.message} ${message}` : message },
258
+ } };
259
+ }
260
+
239
261
  function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
240
262
  const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
241
263
  return {
@@ -553,7 +575,7 @@ export async function restoreNativeCodexAsync(
553
575
  if (sibling) return sibling;
554
576
  try {
555
577
  assertCodexHomeOwner(getCodexHome());
556
- return await restoreNativeCodexAsyncImpl(options);
578
+ return finishApprovedProviderRestore(await restoreNativeCodexAsyncImpl(options), options.revalidateDesiredState);
557
579
  } catch (error) {
558
580
  if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
559
581
  if (!(error instanceof CodexRestoreRefusal)) throw error;
@@ -782,7 +804,7 @@ export function restoreNativeCodex(
782
804
  if (sibling) return sibling;
783
805
  try {
784
806
  assertCodexHomeOwner(getCodexHome());
785
- return restoreNativeCodexImpl(options);
807
+ return finishApprovedProviderRestore(restoreNativeCodexImpl(options), options.revalidateDesiredState);
786
808
  } catch (error) {
787
809
  if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
788
810
  throw error;