@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -776,18 +776,23 @@ function extractSections(raw: string): Map<string, string> {
776
776
  }
777
777
  if (!Array.isArray(parsed)) return sections;
778
778
  for (const item of parsed) {
779
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
779
780
  const content = (item as { content?: unknown }).content;
780
781
  if (!Array.isArray(content)) continue;
781
- const text = content.map(part => String((part as { text?: unknown }).text ?? "")).join("");
782
+ const text = content.map(part => {
783
+ if (!part || typeof part !== "object" || Array.isArray(part)) return "";
784
+ const value = (part as { text?: unknown }).text;
785
+ return typeof value === "string" ? value : "";
786
+ }).join("");
782
787
  // Tag names are NOT all snake_case: Codex renders `<permissions instructions>`
783
788
  // with a space. A `[a-z_]+` pattern silently skipped it, and the layer was
784
789
  // reported as "sent nothing" while its text sat right there.
785
790
  for (const match of text.matchAll(/<([a-zA-Z_][a-zA-Z0-9_ -]*)>([\s\S]*?)<\/\1>/g)) {
786
791
  sections.set(match[1]!, match[2]!.trim());
787
792
  }
788
- // AGENTS.md is NOT tagged: it arrives as a plain `# AGENTS.md instructions
789
- // for <path>` block among the tagged sections. Matching only on tags would
790
- // report the layer as unrendered while its text sits in the same message.
793
+ // AGENTS.md is NOT tagged: it arrives as a `# AGENTS.md instructions`
794
+ // block among the tagged sections. Matching only on tags would report the
795
+ // layer as unrendered while its text sits in the same message.
791
796
  //
792
797
  // Bounded at both ends. Capturing to end-of-message swept up any unrelated
793
798
  // untagged prose that happened to follow, and stripping tag-shaped blocks
@@ -795,8 +800,25 @@ function extractSections(raw: string): Map<string, string> {
795
800
  // AGENTS.md. Codex wraps the body in <INSTRUCTIONS>, so that is the boundary.
796
801
  // No line anchor: the block is concatenated directly onto the previous
797
802
  // section's closing tag, so requiring a newline before it never matched.
798
- const projectDoc = /# AGENTS\.md instructions for [^\n]*\n+<INSTRUCTIONS>\n?([\s\S]*?)\n?<\/INSTRUCTIONS>/.exec(text);
803
+ //
804
+ // The `for <path>` suffix is OPTIONAL. Upstream emits three shapes from
805
+ // `contextual_user_fragment().render()` (core/src/context.rs) and this has
806
+ // to read all three rather than the one that existed when the probe was
807
+ // written: the legacy single-root form `# AGENTS.md instructions for
808
+ // <dir>`, the no-directory form `# AGENTS.md instructions`, and the
809
+ // multi-environment form where each project's block is labeled `for \`<env>\`
810
+ // with root <cwd>` INSIDE the INSTRUCTIONS wrapper. The labels are part of
811
+ // what the model sees, so they stay in the captured body.
812
+ const projectDoc = /# AGENTS\.md instructions(?: for [^\n]*)?\n+<INSTRUCTIONS>\n?([\s\S]*?)\n?<\/INSTRUCTIONS>/.exec(text);
799
813
  if (projectDoc) sections.set("__agents_md", projectDoc[1]!.trim());
814
+ else if (/# AGENTS\.md instructions/.test(text)) {
815
+ // The header rendered but in a shape this extractor does not recognize -
816
+ // a future upstream format, or prose truncated by the output cap. Claiming
817
+ // the layer "sent nothing" would be a lie about a block that is plainly
818
+ // present; the sentinel maps it to `unmapped`, the existing honest answer
819
+ // for "rendered in a form we cannot show".
820
+ sections.set("__agents_md_unmatched", "");
821
+ }
800
822
  }
801
823
  return sections;
802
824
  }
@@ -814,6 +836,13 @@ function mapSectionsToLayers(sections: Map<string, string>): Record<string, Laye
814
836
  ? { text: null, reason: "not-rendered", bytes: 0 }
815
837
  : { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
816
838
  }
839
+ // `__agents_md_unmatched` is the sentinel for "the AGENTS.md header rendered in
840
+ // a shape the bounded extractor does not recognize". `unmapped` is the honest
841
+ // answer for it: the text exists but cannot be shown, which `not-rendered`
842
+ // would deny outright.
843
+ if (layers["agents-md"]?.reason === "not-rendered" && sections.has("__agents_md_unmatched")) {
844
+ layers["agents-md"] = { text: null, reason: "unmapped", bytes: 0 };
845
+ }
817
846
  return layers;
818
847
  }
819
848
 
@@ -319,9 +319,12 @@ export function setAccountQuotaFromParsed(
319
319
  credentialGeneration: historyEvidence.writer.credentialGeneration, windows: historyWindows(historyEvidence.raw),
320
320
  }, updatedAt);
321
321
  }
322
- // Legacy rotation keeps its existing carry behavior, but never inherits policy-only
322
+ // Rotation shares validated main-window retirement, but never inherits policy-only
323
323
  // evidence that outlived its disk TTL. Policy has a separate, identity-checked base.
324
- const next = mergeAccountQuota(quota, legacyExisting, updatedAt);
324
+ const displayQuota: MainPolicyQuotaObservation = isMain && mainWriter && policyQuota?.shortWindowAbsent === true
325
+ ? { ...quota, shortWindowAbsent: true }
326
+ : quota;
327
+ const next = mergeAccountQuota(displayQuota, legacyExisting, updatedAt);
325
328
  accountQuota.set(accountId, next);
326
329
  if (isMain) {
327
330
  if (mainWriter && policyQuota && snapshotHasUsage(policyQuota)) {
@@ -440,7 +443,7 @@ function mergeAccountQuota(
440
443
  }
441
444
  if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt;
442
445
  if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds;
443
- } else if (!policyEvidence || quota.shortWindowAbsent !== true) {
446
+ } else if (quota.shortWindowAbsent !== true) {
444
447
  // Unknown usage is not a lower reading. Retain the entire known tuple: pairing
445
448
  // its percentage with new metadata would silently extend or shorten its reset.
446
449
  // An elapsed reset is the exception. It describes a window that has already rolled over,
@@ -879,14 +882,17 @@ function filterMainPolicyMonthlyQuota(
879
882
  /**
880
883
  * Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages.
881
884
  * A measured long primary, or explicitly absent primary with measured weekly secondary,
882
- * proves replacement only with complete long/null topology. Null supplies no usable evidence.
885
+ * proves replacement with long/null topology; an allowed two-window response may omit tertiary.
886
+ * Null supplies no usable evidence.
883
887
  */
884
888
  export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null {
885
889
  const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
886
890
  if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null;
887
891
  const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type));
888
892
  const [primary, secondary, tertiary] = windows;
889
- // WHAM explicitly reports absent windows as null; omissions cannot prove replacement.
893
+ // Primary and secondary must explicitly report their topology. WHAM also sends
894
+ // two-window responses without tertiary; an explicit allowed verdict disambiguates those
895
+ // from partial observations. A present tertiary still requires measured long/null evidence.
890
896
  // Policy trusts one complete snapshot only when every non-null window is >=24h AND
891
897
  // carries a valid usage reading: a long window without used_percent leaves that
892
898
  // window's usage unknown, and unknown usage must never release a block.
@@ -894,7 +900,8 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu
894
900
  if (quota && (isMeasuredLongWindow(primary)
895
901
  || (primary === null && isMeasuredLongWindow(secondary) && quota.weeklyPercent !== undefined))
896
902
  && (secondary === null || isMeasuredLongWindow(secondary))
897
- && (tertiary === null || isMeasuredLongWindow(tertiary))) {
903
+ && (tertiary === null || isMeasuredLongWindow(tertiary)
904
+ || (tertiary === undefined && data.rate_limit?.allowed === true))) {
898
905
  return { ...quota, shortWindowAbsent: true };
899
906
  }
900
907
  return quota;
@@ -123,7 +123,7 @@ export type RoleProposal = RoleProposalInput & (
123
123
  readonly moveUpIf: string;
124
124
  readonly moveDownIf: string;
125
125
  readonly proposedModel: string | null;
126
- /** Only when the role already carries an effort, unless the caller asks for one always. */
126
+ /** The sized effort on the picked model's ladder; null only when that model has no ladder. */
127
127
  readonly proposedEffort: string | null;
128
128
  readonly reason: string | null;
129
129
  }
@@ -134,7 +134,6 @@ export function buildRoleProposals(
134
134
  roles: readonly RoleProposalInput[],
135
135
  sizing: ReadonlyMap<string, RoleSizingOutcome>,
136
136
  classified: readonly ClassifiedCandidate[],
137
- options: { readonly alwaysProposeEffort?: boolean } = {},
138
137
  ): RoleProposal[] {
139
138
  return roles.map(role => {
140
139
  const outcome = sizing.get(role.role) ?? { unsized: "the role was not sized" };
@@ -151,9 +150,7 @@ export function buildRoleProposals(
151
150
  reason: `no available model is classified ${tier} or above`,
152
151
  };
153
152
  }
154
- const proposedEffort = role.effort === null && !options.alwaysProposeEffort
155
- ? null
156
- : mapEffortToLevel(effort, pick.efforts, pick.defaultEffort);
153
+ const proposedEffort = mapEffortToLevel(effort, pick.efforts, pick.defaultEffort);
157
154
  return { ...sized, status: "proposed", proposedModel: pick.model, proposedEffort, reason: null };
158
155
  });
159
156
  }
@@ -2,6 +2,7 @@ import { saveConfigPreservingClaudeCode } from "../../config";
2
2
  import { clearCodexAccountPin, pinnedCodexAccountId } from "../account-priority";
3
3
  import {
4
4
  POOL_KEY_CODEX,
5
+ clearPoolRotationState,
5
6
  normalizeCodexAccountPoolStrategy,
6
7
  seedPoolRotationAccount,
7
8
  } from "../pool-rotation";
@@ -74,6 +75,26 @@ export function resetCodexRoutingForManualSelection(accountId: string): void {
74
75
  }
75
76
  }
76
77
 
78
+ /**
79
+ * Operator released the selection instead of naming a new account: return routing to
80
+ * automatic without steering. A release names no replacement, so bound threads keep
81
+ * their captured account and quota-avoidance health stands — nothing new overrules it.
82
+ * What still has to go is the steering a selection planted: the one-shot manual
83
+ * preference (releasing account A must not keep blocking every other pick), the
84
+ * runtime cursor, and the rotation seeds on the shared ring and each independent
85
+ * quota-scope ring, which would otherwise force the next pick onto that same account.
86
+ */
87
+ export function resetCodexRoutingForClearedSelection(): void {
88
+ runtimeActiveCodexAccountId = undefined;
89
+ manualPreference.delete(POOL_KEY_CODEX);
90
+ clearPoolRotationState(POOL_KEY_CODEX);
91
+ for (const scope of new Set(Object.values(NATIVE_MODEL_QUOTA_SCOPES))) {
92
+ if (isIndependentCodexQuotaScope(scope)) {
93
+ clearPoolRotationState(codexPoolKeyForScope(scope));
94
+ }
95
+ }
96
+ }
97
+
77
98
  /** Effective active: automatic runtime cursor, else operator/persisted selection. */
78
99
  /**
79
100
  * Unspent operator selections, keyed by pool scope.
@@ -163,7 +163,7 @@ export function deleteScopedHealth(accountId: string, scope: CodexQuotaScope): v
163
163
  }
164
164
 
165
165
  /** Live quota-refusal avoidance for an account, including the lane the request belongs to. */
166
- function codexQuotaAvoidUntil(
166
+ export function codexQuotaAvoidUntil(
167
167
  accountId: string,
168
168
  quotaScope: CodexQuotaScope | undefined,
169
169
  now: number,
@@ -0,0 +1,79 @@
1
+ /**
2
+ * When another stored pool account can serve again, for a main-account hard-lock refusal (#7046).
3
+ *
4
+ * A pool request that fell through to the main login and met the main-account quota guard used
5
+ * to be told to come back at main's own reset, which can be days away while a sibling pool
6
+ * account recovers within hours. This module answers one question: the earliest KNOWN instant at
7
+ * which a stored pool account's every temporary hold ends. It changes no routing state, selects
8
+ * nothing, and never lifts the main guard. The client retries at that time, and ordinary selection
9
+ * decides again from fresh state; a reset timestamp is a hint, not proof of fresh quota.
10
+ *
11
+ * Conservative by construction (the policy Jun is asked to confirm in #7046):
12
+ * - An account is a candidate only when it is a selectable stored pool account that is not
13
+ * paused, plan-excluded, quarantined for sign-in, otherwise unusable, or denied for the model.
14
+ * Those are operator or credential states no clock lifts.
15
+ * - Its recovery time is the LATEST of every routing hold with a known end: quota-health cooldown,
16
+ * quota avoidance, transient soft avoidance, pool-refresh backoff, and the credits-off
17
+ * usage-limit reset. An account under a hold whose end is unknown, or under the sliding
18
+ * failure-window guard (whose end moves with every failure), is skipped rather than guessed.
19
+ * - For an account-gated model, only accounts the resolved entitlement roster admits count, and
20
+ * with no resolved roster nothing does.
21
+ * - An account with no recorded hold is skipped: nothing says when it became unavailable, so
22
+ * nothing says when it will be available again.
23
+ * - Main itself is never a candidate; its own reset stays on the refusal.
24
+ */
25
+ import { isCodexAccountPaused } from "../account-pause";
26
+ import { codexUsageLimitResetAt } from "../account-credit-use";
27
+ import { isSelectableCodexPoolAccount } from "../account-id";
28
+ import { isAccountNeedsReauth } from "../account-runtime-state";
29
+ import { isCodexAccountUsable } from "../account-usability";
30
+ import { getCodexPoolRefreshCooldownUntil } from "../pool-refresh-backoff";
31
+ import { getAccountQuota } from "../quota";
32
+ import type { OcxConfig } from "../../types";
33
+ import { failureWindowSteersNewThreads } from "./failure-window";
34
+ import {
35
+ codexQuotaAvoidUntil, getCodexAccountSoftAvoidUntil, getCodexQuotaHealthSnapshot, type CodexQuotaScope,
36
+ } from "./health-store";
37
+ import { getPoolAccountPlanForSelection, isCodexAccountHeldAtUsageLimit, isCodexAccountPlanExcluded } from "./selection";
38
+
39
+ export function nextCodexPoolRecoveryAt(
40
+ config: OcxConfig,
41
+ now: number,
42
+ quotaScope?: CodexQuotaScope,
43
+ options: {
44
+ deniedAccountIds?: ReadonlySet<string>;
45
+ /** When set, only these accounts may serve the model (account-gated models). */
46
+ eligibleAccountIds?: ReadonlySet<string>;
47
+ } = {},
48
+ ): number | undefined {
49
+ let earliest: number | undefined;
50
+ for (const account of config.codexAccounts ?? []) {
51
+ if (!isSelectableCodexPoolAccount(account)) continue;
52
+ const id = account.id;
53
+ if (options.deniedAccountIds?.has(id) || (options.eligibleAccountIds && !options.eligibleAccountIds.has(id))
54
+ || isCodexAccountPaused(config, id)
55
+ || isCodexAccountPlanExcluded(config, id) || isAccountNeedsReauth(id)
56
+ || !isCodexAccountUsable(config, id) || failureWindowSteersNewThreads(config, id, now)) continue;
57
+ const holds: number[] = [];
58
+ const health = getCodexQuotaHealthSnapshot(id, quotaScope, now);
59
+ if (health) {
60
+ if (typeof health.cooldownUntil !== "number" || !Number.isFinite(health.cooldownUntil)) continue;
61
+ holds.push(health.cooldownUntil);
62
+ }
63
+ const avoidUntil = codexQuotaAvoidUntil(id, quotaScope, now);
64
+ if (avoidUntil !== null) holds.push(avoidUntil);
65
+ const softAvoidUntil = getCodexAccountSoftAvoidUntil(id, now);
66
+ if (softAvoidUntil !== null) holds.push(softAvoidUntil);
67
+ const refreshCooldownUntil = getCodexPoolRefreshCooldownUntil(id, now);
68
+ if (refreshCooldownUntil !== null) holds.push(refreshCooldownUntil);
69
+ if (isCodexAccountHeldAtUsageLimit(config, id, now)) {
70
+ const resetAt = codexUsageLimitResetAt(getAccountQuota(id), getPoolAccountPlanForSelection(config, id), now);
71
+ if (resetAt === undefined) continue;
72
+ holds.push(resetAt);
73
+ }
74
+ if (holds.length === 0) continue;
75
+ const recoveryAt = Math.max(...holds);
76
+ if (recoveryAt > now && (earliest === undefined || recoveryAt < earliest)) earliest = recoveryAt;
77
+ }
78
+ return earliest;
79
+ }
@@ -215,7 +215,7 @@ export {
215
215
  pickAlternateCodexAccount,
216
216
  } from "./routing/selection";
217
217
  export {
218
- resetCodexRoutingForManualSelection,
218
+ resetCodexRoutingForManualSelection, resetCodexRoutingForClearedSelection,
219
219
  getEffectiveActiveCodexAccountId,
220
220
  isEffectiveCodexAccountPinned,
221
221
  } from "./routing/active-account";
@@ -1,13 +1,14 @@
1
1
  import { execFile, execFileSync } from "node:child_process";
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
2
+ import { closeSync, constants, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { delimiter, join } from "node:path";
4
+ import { delimiter, join, posix } from "node:path";
5
5
  import { atomicWriteFile, getConfigDir } from "../config";
6
6
  import { codexExecInvocation, isSpawnableCodexCandidate } from "./exec-invocation";
7
7
  import { resolveCodexHomeDir } from "./home";
8
8
  import { decodeOverlayState } from "./shim-state-file";
9
9
  import { redactSecretString, redactUserPath } from "../lib/redact";
10
10
  import { SUBPROCESS_KILL_GRACE_MS, waitForSubprocessExit } from "../lib/bounded-subprocess";
11
+ import { isPassiveRead } from "../lib/passive-read";
11
12
 
12
13
  export type CodexRuntimeSource =
13
14
  | "environment"
@@ -89,6 +90,8 @@ export interface ResolveCodexRuntimeDeps {
89
90
  execFile?: RuntimeExecFileAsync;
90
91
  existsSync?: (path: string) => boolean;
91
92
  readFileSync?: (path: string, encoding: "utf8") => string;
93
+ /** Bounded in-process macOS bundle identity reader; injected readers bypass the memo. */
94
+ readBundleIdentifier?: (infoPlistPath: string) => string | null;
92
95
  now?: () => number;
93
96
  /**
94
97
  * When false, stop after the first valid priority candidate (skip PATH-wide
@@ -225,10 +228,11 @@ export function loadLastEffortClamp(
225
228
  }
226
229
  }
227
230
 
231
+ /** Return whether a nonempty observation changed, so catalog notices share persistence's identity. */
228
232
  export function persistEffortClamp(
229
233
  diagnostic: EffortClampDiagnostic | null,
230
234
  deps: ResolveCodexRuntimeDeps = {},
231
- ): void {
235
+ ): boolean {
232
236
  const configDir = deps.configDir ?? getConfigDir();
233
237
  const path = codexRuntimeClampStatePath(configDir);
234
238
  if (!diagnostic || diagnostic.removedEfforts.length === 0) {
@@ -237,8 +241,16 @@ export function persistEffortClamp(
237
241
  } catch {
238
242
  /* absent is fine */
239
243
  }
240
- return;
244
+ return false;
241
245
  }
246
+ // This is an observation, not a heartbeat: rewriting updatedAt on every sync both churns disk
247
+ // and makes identical clamps look new after restart. Compare sets, not catalog enumeration order.
248
+ const previous = loadLastEffortClamp(deps);
249
+ const setKey = (values: readonly string[]) => JSON.stringify([...new Set(values)].sort());
250
+ if (previous && previous.runtimePath === diagnostic.runtimePath
251
+ && previous.runtimeVersion === diagnostic.runtimeVersion
252
+ && setKey(previous.removedEfforts) === setKey(diagnostic.removedEfforts)
253
+ && setKey(previous.affectedModels) === setKey(diagnostic.affectedModels)) return false;
242
254
  const payload: PersistedClampState = {
243
255
  version: 1,
244
256
  updatedAt: new Date((deps.now ?? Date.now)()).toISOString(),
@@ -249,6 +261,7 @@ export function persistEffortClamp(
249
261
  };
250
262
  mkdirSync(configDir, { recursive: true, mode: 0o700 });
251
263
  atomicWriteFile(path, `${JSON.stringify(payload, null, 2)}\n`);
264
+ return true;
252
265
  }
253
266
 
254
267
  export function displayCodexRuntimePath(command: string): string {
@@ -425,6 +438,9 @@ function versionProbePrecheck(command: string, deps: ResolveCodexRuntimeDeps): V
425
438
  // `codex --version` here is ~1s of blocking exec per candidate; on the
426
439
  // dashboard probe that cost made Windows report Codex as missing even when
427
440
  // the App install was sitting under LOCALAPPDATA/OpenAI/Codex/bin (issue 4458).
441
+ if (!macCodexAppIdentityMatches(command, deps)) {
442
+ return { ok: false, reason: "Codex app bundle identity mismatch or unavailable" };
443
+ }
428
444
  if (deps.probeVersion === false) return { ok: true, version: null };
429
445
  return null;
430
446
  }
@@ -599,6 +615,161 @@ function pathCandidates(deps: ResolveCodexRuntimeDeps): string[] {
599
615
  return [...new Set(out)];
600
616
  }
601
617
 
618
+ /** Conventional Codex desktop bundles, ordered system before user. */
619
+ function macCodexAppBundles(deps: ResolveCodexRuntimeDeps): string[] {
620
+ const env = deps.env ?? process.env;
621
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
622
+ return ["/Applications/ChatGPT.app", posix.join(home, "Applications/ChatGPT.app")];
623
+ }
624
+
625
+ function macCodexAppCommands(deps: ResolveCodexRuntimeDeps): string[] {
626
+ return macCodexAppBundles(deps).flatMap(bundle => [
627
+ `${bundle}/Contents/Resources/codex-cli/bin/codex`, `${bundle}/Contents/Resources/codex`,
628
+ ]);
629
+ }
630
+
631
+ function realpathOrNull(path: string): string | null {
632
+ try { return realpathSync(path); } catch { return null; }
633
+ }
634
+
635
+ /**
636
+ * Classify any path inside either bundle, including backing executables, by normalized and
637
+ * canonical forms with a segment boundary: a symlinked HOME (or /tmp -> /private/tmp) must not
638
+ * let a canonical bundle path skip the identity check or the handover exclusion.
639
+ */
640
+ function macCodexAppBundleFor(command: string, deps: ResolveCodexRuntimeDeps): string | undefined {
641
+ if ((deps.platform ?? process.platform) !== "darwin" || !posix.isAbsolute(command)) return undefined;
642
+ const forms = [posix.normalize(command), realpathOrNull(command)].filter((form): form is string => Boolean(form));
643
+ return macCodexAppBundles(deps).find(bundle => {
644
+ const roots = [posix.normalize(bundle), realpathOrNull(bundle)].filter((root): root is string => Boolean(root));
645
+ return forms.some(form => roots.some(root => form.startsWith(`${root}/`)));
646
+ });
647
+ }
648
+
649
+ function macCodexAppIdentityMatches(command: string, deps: ResolveCodexRuntimeDeps): boolean {
650
+ const bundle = macCodexAppBundleFor(command, deps);
651
+ if (!bundle) return true;
652
+ try {
653
+ return (deps.readBundleIdentifier ?? readBoundedBundleIdentifier)(`${bundle}/Contents/Info.plist`) === "com.openai.codex";
654
+ } catch { return false; }
655
+ }
656
+
657
+ /** XML plist subset only: no external entity expansion, subprocess, or unbounded read. */
658
+ function readBoundedBundleIdentifier(path: string): string | null {
659
+ let fd: number | undefined;
660
+ try {
661
+ fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
662
+ const st = fstatSync(fd);
663
+ const limit = 64 * 1024;
664
+ if (!st.isFile() || st.size > limit) return null;
665
+ const bytes = Buffer.alloc(limit + 1);
666
+ let size = 0;
667
+ while (size < bytes.length) {
668
+ const count = readSync(fd, bytes, size, bytes.length - size, null);
669
+ if (!count) break;
670
+ size += count;
671
+ }
672
+ if (size > limit) return null;
673
+ // XML 1.0 Char: tab, LF, CR, U+0020-D7FF, U+E000-FFFD, U+10000-10FFFF (no surrogates after fatal decoding).
674
+ const xmlChar = (code: number): boolean => code === 9 || code === 10 || code === 13
675
+ || (code >= 0x20 && code <= 0xd7ff) || (code >= 0xe000 && code <= 0xfffd) || (code >= 0x10000 && code <= 0x10ffff);
676
+ const allXmlChars = (text: string): boolean => [...text].every(char => xmlChar(char.codePointAt(0)!));
677
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, size));
678
+ // Every character is checked before anything is stripped, the prolog included.
679
+ if (!allXmlChars(decoded)) return null;
680
+ // Only a well-formed UTF-8 declaration and Apple's DOCTYPE are accepted, anchored in the prolog, in that order.
681
+ const declaration = /^<\?xml\s+version=(["'])1\.[0-9]\1(?:\s+encoding=(["'])utf-8\2)?(?:\s+standalone=(["'])(?:yes|no)\3)?\s*\?>/i;
682
+ if (decoded.startsWith("<?") && !declaration.test(decoded)) return null;
683
+ const xml = decoded
684
+ .replace(declaration, "")
685
+ .replace(/^\s*<!DOCTYPE plist PUBLIC "-\/\/Apple\/\/DTD PLIST 1\.0\/\/EN" "http:\/\/www\.apple\.com\/DTDs\/PropertyList-1\.0\.dtd">/, "");
686
+ type Node = { name: string; text: string; children: Node[] };
687
+ const root: Node = { name: "root", text: "", children: [] };
688
+ const stack = [root];
689
+ const tokens = xml.match(/<!--[\s\S]*?-->|<[^>]*>|[^<]+/g) ?? [];
690
+ if (tokens.join("") !== xml) return null;
691
+ for (const token of tokens) {
692
+ if (token.startsWith("<!--")) {
693
+ // A comment may not contain "--" or end in "-" (XML 1.0 §2.5).
694
+ if (/--|-$/.test(token.slice(4, -3))) return null;
695
+ continue;
696
+ }
697
+ const parent = stack[stack.length - 1]!;
698
+ if (!token.startsWith("<")) {
699
+ if (token.includes("]]>")) return null;
700
+ parent.text += token;
701
+ continue;
702
+ }
703
+ const close = /^<\/(\w+)\s*>$/.exec(token);
704
+ if (close) {
705
+ if (stack.length === 1 || parent.name !== close[1]) return null;
706
+ stack.pop();
707
+ continue;
708
+ }
709
+ const open = /^<(plist|dict|array|key|string|integer|real|date|data|true|false)(?:\s+version="1\.0")?\s*(\/?)>$/.exec(token);
710
+ if (!open || (token.includes("version=") && open[1] !== "plist")) return null;
711
+ const node: Node = { name: open[1]!, text: "", children: [] };
712
+ parent.children.push(node);
713
+ if (!open[2]) stack.push(node);
714
+ }
715
+ if (stack.length !== 1 || root.text.trim() || root.children.length !== 1) return null;
716
+ const plist = root.children[0]!;
717
+ if (plist.name !== "plist" || plist.text.trim() || plist.children.length !== 1) return null;
718
+ const dict = plist.children[0]!;
719
+ if (dict.name !== "dict" || dict.text.trim() || dict.children.length % 2) return null;
720
+ const decode = (text: string): string => text.replace(/&([^;]*);|&/g, (_match, entity: string | undefined) => {
721
+ const named: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
722
+ if (entity && Object.hasOwn(named, entity)) return named[entity]!;
723
+ if (!entity || !/^#(?:[0-9]+|x[0-9a-fA-F]+)$/.test(entity)) throw new Error("invalid XML entity");
724
+ const value = entity.startsWith("#x") ? parseInt(entity.slice(2), 16) : Number(entity.slice(1));
725
+ if (!xmlChar(value)) throw new Error("invalid XML character");
726
+ return String.fromCodePoint(value);
727
+ });
728
+ // The identity only counts inside a document that is valid as a whole: every node, entity and
729
+ // literal character is checked, not just the CFBundleIdentifier pair.
730
+ const leafFormats: Record<string, RegExp> = {
731
+ integer: /^\s*[+-]?(?:\d+|0x[0-9a-fA-F]+)\s*$/,
732
+ real: /^\s*(?:[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|[+-]?(?:nan|inf(?:inity)?))\s*$/i,
733
+ date: /^\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\s*$/,
734
+ data: /^[A-Za-z0-9+/=\s]*$/,
735
+ };
736
+ const validNode = (node: Node, inDict: "key" | "value" | null): boolean => {
737
+ if (!allXmlChars(node.text)) return false;
738
+ if (inDict === "key" && node.name !== "key") return false;
739
+ if (inDict !== "key" && node.name === "key") return false;
740
+ switch (node.name) {
741
+ case "dict":
742
+ return !node.text.trim() && node.children.length % 2 === 0
743
+ && node.children.every((child, index) => validNode(child, index % 2 === 0 ? "key" : "value"));
744
+ case "array":
745
+ return !node.text.trim() && node.children.every(child => validNode(child, null));
746
+ case "key": case "string":
747
+ decode(node.text);
748
+ return node.children.length === 0;
749
+ case "true": case "false":
750
+ return node.children.length === 0 && !node.text.trim();
751
+ case "integer": case "real": case "date": case "data":
752
+ return node.children.length === 0 && leafFormats[node.name]!.test(decode(node.text));
753
+ default:
754
+ return false;
755
+ }
756
+ };
757
+ if (!validNode(dict, null)) return null;
758
+ let identifier: string | null = null;
759
+ let keys = 0;
760
+ for (let i = 0; i < dict.children.length; i += 2) {
761
+ const key = dict.children[i]!, value = dict.children[i + 1]!;
762
+ if (key.name !== "key" || key.children.length) return null;
763
+ if (decode(key.text) !== "CFBundleIdentifier") continue;
764
+ keys++;
765
+ if (value.name !== "string" || value.children.length) return null;
766
+ identifier = decode(value.text);
767
+ }
768
+ return keys === 1 ? identifier : null;
769
+ } catch { return null; }
770
+ finally { if (fd !== undefined) closeSync(fd); }
771
+ }
772
+
602
773
  /**
603
774
  * Codex installs that PATH does not necessarily expose.
604
775
  *
@@ -647,6 +818,10 @@ function installedCodexCandidates(deps: ResolveCodexRuntimeDeps): string[] {
647
818
  "/usr/local/bin/codex",
648
819
  "/opt/homebrew/bin/codex",
649
820
  ];
821
+ if (platform === "darwin") {
822
+ const exists = deps.existsSync ?? existsSync;
823
+ return [...posix, ...macCodexAppCommands(deps).filter(command => exists(command) && macCodexAppIdentityMatches(command, deps))];
824
+ }
650
825
  if (platform !== "linux") return posix;
651
826
  // Windows Codex Desktop in WSL app-server mode ships its Linux binary under the
652
827
  // effective Codex home as bin/wsl/<version-hash>/codex, and a Desktop update replaces
@@ -839,6 +1014,7 @@ function resolveCacheKey(deps: ResolveCodexRuntimeDeps): string | null {
839
1014
  || deps.execFile
840
1015
  || deps.existsSync
841
1016
  || deps.readFileSync
1017
+ || deps.readBundleIdentifier
842
1018
  || deps.readdirSync
843
1019
  || deps.statSync
844
1020
  || deps.configDir
@@ -974,7 +1150,8 @@ export function getCodexRuntimeSnapshot(): ResolveCodexRuntimeResult {
974
1150
  const cacheKey = resolveCacheKey({});
975
1151
  const memo = resolveCache?.key === cacheKey ? resolveCache : null;
976
1152
  if (memo && Date.now() - memo.at < RESOLVE_CACHE_MS) return cloneAndDeepFreeze(memo.value);
977
- resolveCodexRuntimeAsync().catch(() => { /* the next read retries */ });
1153
+ // A passive read (a Remote Link Child's, #6959) starts no version probes on this machine.
1154
+ if (!isPassiveRead()) resolveCodexRuntimeAsync().catch(() => { /* the next read retries */ });
978
1155
  if (memo) return cloneAndDeepFreeze(memo.value);
979
1156
  return resolveCodexRuntime({ discoverAlternatives: false, probeVersion: false });
980
1157
  }
@@ -1020,10 +1197,18 @@ function* resolveCodexRuntimeSteps(
1020
1197
  for (const command of pathCandidates(deps)) {
1021
1198
  ordered.push({ command, source: "path" });
1022
1199
  }
1023
- for (const command of installedCodexCandidates(deps)) {
1200
+ const installed = installedCodexCandidates(deps);
1201
+ // Order by the generated app commands only: an established install path that links into the
1202
+ // app keeps its precedence, while identity and handover use the canonical classification.
1203
+ const appCommands = new Set(macCodexAppCommands(deps));
1204
+ const appCandidates = installed.filter(command => appCommands.has(command));
1205
+ for (const command of installed.filter(command => !appCommands.has(command))) {
1024
1206
  ordered.push({ command, source: "installed" });
1025
1207
  }
1026
1208
  ordered.push({ command: "codex", source: "fallback" });
1209
+ for (const command of appCandidates) {
1210
+ ordered.push({ command, source: "installed" });
1211
+ }
1027
1212
 
1028
1213
  const seen = new Set<string>();
1029
1214
  const valid: ResolvedCodexRuntime[] = [];
@@ -1045,13 +1230,16 @@ function* resolveCodexRuntimeSteps(
1045
1230
  // there is then nothing to compare it against.
1046
1231
  const persistedIsUnpinned = Boolean(persisted?.command) && !persistedCodexRuntimeIsPinned(persisted);
1047
1232
  for (const candidate of ordered) {
1233
+ // Deferred resolution accepts the bare fallback without proving it exists; when a verified
1234
+ // app CLI is present it must not preempt that app. Other platforms keep the old behaviour.
1235
+ if (candidate.source === "fallback" && deps.probeVersion === false && appCandidates.length > 0) continue;
1048
1236
  const key = candidate.command.toLowerCase();
1049
1237
  if (seen.has(key)) continue;
1050
1238
  seen.add(key);
1051
1239
  if (
1052
1240
  deps.discoverAlternatives === false
1053
1241
  && valid.length > 0
1054
- && !(persistedIsUnpinned && candidate.source === "installed")
1242
+ && !(persistedIsUnpinned && candidate.source === "installed" && !macCodexAppBundleFor(candidate.command, deps))
1055
1243
  ) {
1056
1244
  continue;
1057
1245
  }
@@ -1111,6 +1299,7 @@ function* resolveCodexRuntimeSteps(
1111
1299
  const newerDiscovered = valid
1112
1300
  .filter(item =>
1113
1301
  !sameRuntimeCommand(item.command, selected.command)
1302
+ && !macCodexAppBundleFor(item.command, deps)
1114
1303
  && typeof item.version === "string"
1115
1304
  && typeof selected.version === "string"
1116
1305
  && compareCodexVersions(item.version, selected.version) > 0)
@@ -1232,6 +1421,5 @@ export function formatClampLogLines(diagnostic: EffortClampDiagnostic): string[]
1232
1421
  const efforts = diagnostic.removedEfforts.join(", ");
1233
1422
  return [
1234
1423
  `[opencodex] Removed unsupported reasoning efforts: ${efforts}`,
1235
- "[opencodex] Run ocx doctor for diagnosis and recovery.",
1236
1424
  ];
1237
1425
  }
@@ -29,6 +29,12 @@ export interface CodexShimDiagnostic {
29
29
  summary: string;
30
30
  runnable?: boolean;
31
31
  active?: boolean | null;
32
+ /**
33
+ * PATH overlay only: the wrapper and its private state are intact and runnable, but this process
34
+ * could not see it first on PATH. Reinstalling cannot change that; the shell must source the
35
+ * environment file. The dashboard's PATH can also differ from the user's shell (#6971).
36
+ */
37
+ activationPending?: boolean;
32
38
  }
33
39
 
34
40
  /** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */