@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
@@ -44,6 +44,10 @@ export function overlayActivationHint(configDir = getConfigDir()): string {
44
44
  function sameId(left: OverlayIdentity | null | undefined, right: OverlayIdentity | null | undefined): boolean {
45
45
  return !!left && !!right && left.dev === right.dev && left.ino === right.ino;
46
46
  }
47
+ /** APFS device numbers can change after reboot; persisted ownership still requires the same inode. */
48
+ function samePersistedInode(current: OverlayIdentity, recorded: OverlayIdentity | undefined): boolean {
49
+ return !!recorded && current.ino === recorded.ino;
50
+ }
47
51
  function lexical(path: string): Stats | null {
48
52
  try { return lstatSync(path); }
49
53
  catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
@@ -128,7 +132,7 @@ function shellEnvironment(): string {
128
132
  'unset ocx_shim_dir ocx_shim_rest ocx_shim_path ocx_shim_more ocx_shim_part', ""].join("\n");
129
133
  }
130
134
  function ownedWrapper(value: Snapshot, state: OverlayState): boolean {
131
- return sameId(value.identity, state.wrapperIdentity)
135
+ return samePersistedInode(value.identity, state.wrapperIdentity)
132
136
  && value.content.includes(SHIM_MARKER) && value.content.includes(`exec ${shQuote(state.launcherPath)} "$@"`);
133
137
  }
134
138
  function requireOverlayIdentities(state: OverlayState): void {
@@ -137,7 +141,7 @@ function requireOverlayIdentities(state: OverlayState): void {
137
141
  }
138
142
  }
139
143
  function ownedEnvironment(value: Snapshot, state: OverlayState): boolean {
140
- return sameId(value.identity, state.envIdentity) && value.content === shellEnvironment();
144
+ return samePersistedInode(value.identity, state.envIdentity) && value.content === shellEnvironment();
141
145
  }
142
146
  function revalidateOverlay(state: OverlayState, saved: Snapshot | null, wrapper: Snapshot | null, env: Snapshot | null): void {
143
147
  if (!unchanged(statePath(), saved) || !unchanged(state.wrapperPath, wrapper) || !unchanged(overlayPaths().env, env)) {
@@ -201,6 +205,7 @@ export function overlayDiagnostic(state: ShimState): CodexShimDiagnostic {
201
205
  inspection?.revalidate();
202
206
  } catch (error) { active = null; runnable = false; integrityMessage = failure(error).message; }
203
207
  return { installed: true, healthy: complete && runnable && active === true, runnable, active,
208
+ activationPending: complete && runnable && active !== true,
204
209
  summary: `Codex PATH shim: ${complete && runnable ? "ready" : "unhealthy"} at ${state.wrapperPath}; launcher ${state.launcherPath}; PATH ${active === null ? "unverified" : active ? "active" : "inactive"}. ${complete && runnable ? overlayActivationHint() : integrityMessage || "Run ocx codex-shim install to repair the shim."}` };
205
210
  }
206
211
 
@@ -404,7 +409,9 @@ function failure(error: unknown): InstallResult {
404
409
  const message = error instanceof Error ? error.message : "Codex overlay operation failed";
405
410
  return { installed: false, refused: true, message: truncateRetainedUtf8(message.replace(/[\r\n]/g, " "), MAX_DIAGNOSTIC_VALUE_BYTES) };
406
411
  }
407
- function installLocked(discover: () => string | null, automatic: boolean, check: () => void): InstallResult {
412
+ function installLocked(
413
+ discover: () => string | null, automatic: boolean, check: () => void, describeMiss?: () => string | null,
414
+ ): InstallResult {
408
415
  recoverJournal(check);
409
416
  const pinnedState = snapshot(statePath());
410
417
  const result = readStateResult();
@@ -428,18 +435,20 @@ function installLocked(discover: () => string | null, automatic: boolean, check:
428
435
  const discovered = automatic ? null : discover();
429
436
  launcher = discovered ? resolve(discovered) : null;
430
437
  }
431
- if (!launcher) throw new Error("Native Codex launcher not found; install or repair Codex with its package manager");
438
+ // A discovery that refused a candidate (fnm's temporary multishell path) says why; reporting it
439
+ // as plain absence sent users to reinstall a Codex that was already there (#6963).
440
+ if (!launcher) throw new Error(describeMiss?.() || "Native Codex launcher not found; install or repair Codex with its package manager");
432
441
  check();
433
442
  return publishOverlay(launcher, overlay, check, pinnedState);
434
443
  }
435
- export function installUnixOverlay(discover: () => string | null): InstallResult {
444
+ export function installUnixOverlay(discover: () => string | null, describeMiss?: () => string | null): InstallResult {
436
445
  let lock: ReturnType<typeof tryAcquireShimRestoreLock> = null;
437
446
  try {
438
447
  const check = namespace(true);
439
448
  lock = tryAcquireShimRestoreLock();
440
449
  if (!lock) throw new Error("Codex shim operation is already in progress; retry later");
441
450
  check();
442
- return installLocked(discover, false, check);
451
+ return installLocked(discover, false, check, describeMiss);
443
452
  } catch (error) { return failure(error); }
444
453
  finally { lock?.release(); }
445
454
  }
package/src/codex/shim.ts CHANGED
@@ -1079,7 +1079,7 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
1079
1079
  }
1080
1080
 
1081
1081
  export function installCodexShim(deps: RuntimePreflightDeps = {}): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
1082
- if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath);
1082
+ if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath, lastCodexDiscoveryError);
1083
1083
  return installCodexShimInternal({ ...deps, allowFreshInstall: true });
1084
1084
  }
1085
1085
 
@@ -1,6 +1,7 @@
1
1
  import { parseResetCooldownMs } from "../codex/routing";
2
2
  import { classifyError, isCyberPolicyCode } from "../lib/errors";
3
3
  import { isNonReplayableUpstreamCode } from "../lib/upstream-retry";
4
+ import { isAnthropicBillingFailure } from "../providers/anthropic-billing-refusal";
4
5
  import type { OcxComboTarget } from "../types";
5
6
  import { targetKey } from "./types";
6
7
  import {
@@ -563,6 +564,7 @@ export function comboFailureCooldownScope(
563
564
  options?: { code?: string | null },
564
565
  ): ComboFailureCooldownScope {
565
566
  const code = normalizedFailureCode(options?.code);
567
+ if (isAnthropicBillingFailure(status, message, options?.code)) return "provider";
566
568
  // Request-shape refusals first: an oversized request must not cool a healthy target.
567
569
  // A native transport can surface a zero-output model overflow as a generic
568
570
  // upstream_server_error carrying precise context-window prose, so consult the bounded
@@ -847,6 +849,7 @@ export function comboFailureDecision(
847
849
  // reaches that list and returns terminal before anything can ask whether the next target
848
850
  // could have served the request (#4903).
849
851
  if (isResponseFormatCapabilityRefusal(status, message, options?.code)) return "hop";
852
+ if (isAnthropicBillingFailure(status, message, options?.code)) return "hop";
850
853
  if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
851
854
  return "stop";
852
855
  }
@@ -0,0 +1,34 @@
1
+ type KeyRecord = Record<string, unknown> & { id: string };
2
+
3
+ function index(value: unknown): Map<string, KeyRecord> | null {
4
+ if (value === undefined) return new Map();
5
+ if (!Array.isArray(value)) return null;
6
+ const rows = new Map<string, KeyRecord>();
7
+ for (const row of value) {
8
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.id !== "string" || !row.id || rows.has(row.id)) return null;
9
+ rows.set(row.id, row);
10
+ }
11
+ return rows;
12
+ }
13
+
14
+ /** Stable-ID three-way merge: either side's revocation defeats stale edits to that ID. */
15
+ export function reconcileApiKeys(
16
+ baseline: unknown, live: unknown, persisted: unknown,
17
+ mergeRecord: (live: Record<string, unknown>, baseline: Record<string, unknown>, persisted: Record<string, unknown>) => void,
18
+ ): unknown[] | null {
19
+ if (baseline === undefined && live === undefined && persisted === undefined) return null;
20
+ const before = index(baseline), pending = index(live), disk = index(persisted);
21
+ if (!before || !pending || !disk) return null;
22
+ const merged: KeyRecord[] = [];
23
+ const liveOrder = [...pending.keys()], diskOrder = [...disk.keys()];
24
+ const order = JSON.stringify(liveOrder) === JSON.stringify([...before.keys()])
25
+ ? [...diskOrder, ...liveOrder] : [...liveOrder, ...diskOrder];
26
+ for (const id of new Set(order)) {
27
+ const local = pending.get(id), saved = disk.get(id);
28
+ if (before.has(id) && (!local || !saved)) continue;
29
+ const row = structuredClone(local ?? saved!);
30
+ if (local && saved) mergeRecord(row, before.get(id) ?? {}, saved);
31
+ merged.push(row);
32
+ }
33
+ return merged;
34
+ }
@@ -6,6 +6,7 @@ import { lstatSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import * as z from "zod/v4";
8
8
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
9
+ import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
9
10
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
10
11
  import type { OcxConfig } from "../types";
11
12
  import { anthropicAccountPoolConfigError, anthropicOAuthInstanceConfigError, anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
@@ -648,6 +649,21 @@ export function metricsExportConfigError(value: unknown): string | null {
648
649
  return null;
649
650
  }
650
651
 
652
+ /** Load degrades a malformed service health recovery section to off; live writes reject it. */
653
+ export function serviceHealthRecoveryConfigError(value: unknown): string | null {
654
+ const raw = rawConfigRecord(value);
655
+ if (!raw || !Object.hasOwn(raw, "serviceHealthRecovery") || raw.serviceHealthRecovery === undefined) return null;
656
+ const section = rawConfigRecord(raw.serviceHealthRecovery);
657
+ if (!section) return "schema_invalid: serviceHealthRecovery: must be an object or omitted";
658
+ if (Object.keys(section).some(key => key !== "enabled")) {
659
+ return "schema_invalid: serviceHealthRecovery: contains an unsupported field";
660
+ }
661
+ if (section.enabled !== undefined && typeof section.enabled !== "boolean") {
662
+ return "schema_invalid: serviceHealthRecovery.enabled: must be a boolean";
663
+ }
664
+ return null;
665
+ }
666
+
651
667
 
652
668
  function skillsConfigError(value: unknown): string | null {
653
669
  const raw = rawConfigRecord(value);
@@ -679,7 +695,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
679
695
  const anthropicError = anthropicOAuthInstanceConfigError(value) ?? anthropicAccountPoolConfigError(value) ?? anthropicSidecarConfigError(value);
680
696
  if (anthropicError) return { ok: false, error: anthropicError };
681
697
  const boundaryError = blockedModelRedirectsError(value)
682
- ?? compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
698
+ ?? compactionRecoveryConfigError(value) ?? desktopCompatibilityConfigError(value) ?? configReasoningPinsConfigError(value)
683
699
  ?? blankHostnameError(value)
684
700
  ?? (rawSubagentModelForce(value) !== undefined && !isSubagentModelEntry(rawSubagentModelForce(value)) ? "schema_invalid: claudeCode.subagentModelForce: expected a safe roster-style model id" : null)
685
701
  ?? claudeSubagentEffortError(value)
@@ -710,6 +726,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
710
726
  ?? loopbackListenerPortError(value)
711
727
  ?? managementIngressConfigError(value)
712
728
  ?? metricsExportConfigError(value)
729
+ ?? serviceHealthRecoveryConfigError(value)
713
730
  ?? skillsConfigError(value);
714
731
  if (boundaryError) return { ok: false, error: boundaryError };
715
732
  const result = configSchema.safeParse(value);
@@ -17,6 +17,11 @@ export function metricsExportEnabled(config: Pick<OcxConfig, "metricsExport">):
17
17
  return config.metricsExport?.enabled === true;
18
18
  }
19
19
 
20
+ /** Default-off service health recovery (#6671); read once when the proxy starts. */
21
+ export function serviceHealthRecoveryEnabled(config: Pick<OcxConfig, "serviceHealthRecovery">): boolean {
22
+ return config.serviceHealthRecovery?.enabled === true;
23
+ }
24
+
20
25
  /**
21
26
  * Default cadence for catalog auto-refresh (issue #3630): one converge pass
22
27
  * per hour. Each pass spends a live /models call against every enabled provider, and
@@ -18,6 +18,8 @@ import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } f
18
18
  import { ConfigWritePublishedError, persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
19
19
  import { configDiagnosticsFromRaw, readConfigDiagnostics } from "./diagnostics";
20
20
  import { normalizePersistedClaudeCode } from "./load-degrade";
21
+ import { reconcileApiKeys } from "./api-key-reconcile";
22
+ import { captureLiveTopologyBaseline, LIVE_TOPOLOGY_KEYS, projectLiveTopology } from "./live-topology";
21
23
 
22
24
  // ---------------------------------------------------------------------------
23
25
  // Hand-edit protection for the `claudeCode` subtree (devlog 260726_claude_auth_auto/040 H1).
@@ -104,6 +106,7 @@ const detachedConfigSnapshots = new WeakSet<OcxConfig>();
104
106
  export function armClaudeCodeBaseline(config: OcxConfig): void {
105
107
  liveConfigBaseline.set(config, structuredClone(config));
106
108
  claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
109
+ captureLiveTopologyBaseline(config);
107
110
  }
108
111
 
109
112
  /**
@@ -135,6 +138,19 @@ export function adoptPersistedProviderIntoLiveConfig(
135
138
  if (persistedConfig) refreshPreservedProviderOwner(config, persistedConfig);
136
139
  }
137
140
 
141
+ /** Adopt only the committed next-start preference; preserve unrelated pending live edits. */
142
+ export function adoptPersistedDesktopCompatibility(config: OcxConfig, persisted: OcxConfig["desktopCompatibility"]): void {
143
+ const baseline = liveConfigBaseline.get(config);
144
+ const merged = reconcileConfigValue(baseline?.desktopCompatibility ?? MISSING_CONFIG_VALUE,
145
+ config.desktopCompatibility ?? MISSING_CONFIG_VALUE, persisted ?? MISSING_CONFIG_VALUE);
146
+ if (merged === MISSING_CONFIG_VALUE) delete config.desktopCompatibility;
147
+ else config.desktopCompatibility = merged as OcxConfig["desktopCompatibility"];
148
+ if (baseline) {
149
+ if (persisted === undefined) delete baseline.desktopCompatibility;
150
+ else baseline.desktopCompatibility = structuredClone(persisted);
151
+ }
152
+ }
153
+
138
154
  /** Test seam only: is this instance armed? */
139
155
  export function claudeCodeBaselineArmed(config: OcxConfig): boolean {
140
156
  return claudeCodeBaseline.has(config);
@@ -321,7 +337,12 @@ function reconcileConfigRecord(
321
337
  && baselineValue !== MISSING_CONFIG_VALUE
322
338
  && persistedValue === MISSING_CONFIG_VALUE
323
339
  ? MISSING_CONFIG_VALUE
324
- : key === "customModels"
340
+ : key === "apiKeys"
341
+ ? reconcileApiKeys(baselineValue === MISSING_CONFIG_VALUE ? undefined : baselineValue,
342
+ liveValue === MISSING_CONFIG_VALUE ? undefined : liveValue,
343
+ persistedValue === MISSING_CONFIG_VALUE ? undefined : persistedValue, reconcileConfigRecord)
344
+ ?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
345
+ : key === "customModels"
325
346
  ? reconcileCustomModels(baselineValue, liveValue, persistedValue)
326
347
  ?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
327
348
  : key === "disabledModels"
@@ -401,7 +422,7 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
401
422
  config as unknown as Record<string, unknown>,
402
423
  persistedBaseline as unknown as Record<string, unknown>,
403
424
  persisted as unknown as Record<string, unknown>,
404
- new Set(["hostname", "port", ...(claudeGuardArmed ? ["claudeCode"] : [])]),
425
+ new Set([...LIVE_TOPOLOGY_KEYS, ...(claudeGuardArmed ? ["claudeCode"] : [])]),
405
426
  );
406
427
  applyConfigObjectChildDeletions(config, childDeletions);
407
428
 
@@ -473,36 +494,32 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
473
494
  // custom-model deletion migration. A second read could observe different bytes.
474
495
  const onDisk = readRawConfigJson();
475
496
  const baseline = liveConfigBaseline.get(config);
497
+ let diskDeletedKeys = new Set<string>();
498
+ let persistedSnapshot: OcxConfig | undefined;
476
499
  if (baseline && onDisk !== undefined) {
477
500
  const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
478
501
  if (persistedDiagnostics.source === "file") {
479
- // A detached snapshot diverged only where this pipeline mutated it (model
480
- // discovery fields for the auto-refresh tick): it cannot express a
481
- // deliberate deletion, so the disk-only-key and listener-binding skips
482
- // below would only discard concurrent hand edits. It merges every top-level
483
- // key — including configRebaseProvenance, so a cooperating writer's
484
- // deletion marker adopted from disk is honored instead of silently dropped —
485
- // and captures deletion intent from the current disk snapshot before the
486
- // merge can temporarily restore a key and invalidate its deletion marker.
502
+ persistedSnapshot = persistedDiagnostics.config;
503
+ // Capture authoritative disk deletions before reconciliation can restore a key.
504
+ // Live instances retain actual socket identity; detached discovery snapshots
505
+ // own no socket and also merge listener fields and disk-only sections.
487
506
  const detached = detachedConfigSnapshots.has(config);
488
- const deletedKeys = detached
489
- ? configRebaseDeletionKeys(persistedDiagnostics.config)
490
- : configRebaseDeletionKeys(config);
507
+ diskDeletedKeys = configRebaseDeletionKeys(persistedDiagnostics.config);
508
+ // Preserve explicit pending live edits; stale retention is not an edit.
509
+ if (!detached) for (const key of diskDeletedKeys) {
510
+ if (!LIVE_TOPOLOGY_KEYS.has(key) && !deepEqual(
511
+ ownConfigValue(config as unknown as Record<string, unknown>, key),
512
+ ownConfigValue(baseline as unknown as Record<string, unknown>, key))) diskDeletedKeys.delete(key);
513
+ }
514
+ const deletedKeys = new Set([...diskDeletedKeys, ...configRebaseDeletionKeys(config)]);
491
515
  const skipped = detached
492
516
  ? new Set(["claudeCode"])
493
- : new Set(["hostname", "port", "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
517
+ : new Set([...LIVE_TOPOLOGY_KEYS, "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
494
518
  if (!detached) {
495
- const provenanceExists = configHasRebaseProvenance(config);
496
- // Only keys this live config is actually known to have diverged on may be
497
- // rebased. The baseline is captured once when the server arms it, so any key
498
- // that appeared on disk afterwards — through saveConfig(), a hand edit, or
499
- // another process — is absent from the baseline as well as from the live
500
- // config. Reconciling those keys reads "live never changed this" and adopts
501
- // the disk value, which resurrects a field the live writer had deliberately
502
- // deleted (#1462 regression: PUT /api/grok/selection with an empty list).
503
- // Restrict the merge to keys the baseline knew about, plus keys the live
504
- // config still carries; a key that exists only on disk is left to the
505
- // ordinary whole-config write below.
519
+ const provenanceExists = configHasRebaseProvenance(config) || configHasRebaseProvenance(persistedDiagnostics.config);
520
+ // Without provenance, disk-only keys remain subject to the legacy whole-
521
+ // config write: adopting them could resurrect a deliberate live deletion
522
+ // absent from its baseline (#1462). Provenance makes that intent explicit.
506
523
  const rebaseableKeys = new Set([
507
524
  ...Object.keys(baseline as unknown as Record<string, unknown>),
508
525
  ...Object.keys(config as unknown as Record<string, unknown>),
@@ -521,6 +538,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
521
538
  skipped,
522
539
  );
523
540
  for (const key of deletedKeys) {
541
+ if (!detached && LIVE_TOPOLOGY_KEYS.has(key)) continue;
524
542
  delete (config as unknown as Record<string, unknown>)[key];
525
543
  }
526
544
  }
@@ -537,15 +555,23 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
537
555
  }
538
556
  }
539
557
  }
540
- const provenanceProjection = projectConfigRebaseProvenance(config);
558
+ const desired = persistedSnapshot && !detachedConfigSnapshots.has(config)
559
+ ? projectLiveTopology(config, persistedSnapshot, reconcileConfigRecord) : config;
560
+ const durableProjection = { ...desired, configRebaseProvenance: projectConfigRebaseProvenance(config).configRebaseProvenance };
561
+ for (const key of diskDeletedKeys) {
562
+ delete (durableProjection as unknown as Record<string, unknown>)[key];
563
+ }
564
+ if (diskDeletedKeys.size) durableProjection.configRebaseProvenance = { version: 1,
565
+ deletedTopLevelKeys: [...new Set([...configRebaseDeletionKeys(durableProjection), ...diskDeletedKeys])].sort() };
566
+ const provenanceProjection = projectConfigRebaseProvenance(durableProjection);
541
567
  const projectedConfig = projectCustomModelCatalogMigration(
542
568
  onDisk,
543
- config,
569
+ durableProjection,
544
570
  );
545
571
  if (provenanceProjection.configRebaseProvenance === undefined) delete projectedConfig.configRebaseProvenance;
546
572
  else projectedConfig.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
547
- const persistedBinding = bindingBaseline && onDisk
548
- ? readPersistedServerBinding(onDisk, bindingBaseline)
573
+ const persistedBinding = (bindingBaseline || diskDeletedKeys.has("hostname")) && onDisk
574
+ ? readPersistedServerBinding(onDisk, bindingBaseline ?? { port: config.port, hostname: config.hostname })
549
575
  : bindingBaseline;
550
576
  if (persistedBinding) {
551
577
  const persistedConfig: OcxConfig = { ...projectedConfig, port: persistedBinding.port };
@@ -557,6 +583,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
557
583
  persist(projectedConfig);
558
584
  }
559
585
  adoptCustomModelCatalogMigration(config, projectedConfig);
586
+ captureLiveTopologyBaseline(config);
560
587
  if (claudeCodeBaseline.has(config)) {
561
588
  claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
562
589
  }
@@ -0,0 +1,29 @@
1
+ import type { OcxConfig } from "../types";
2
+
3
+ export const LIVE_TOPOLOGY_KEYS = new Set(["hostname", "port", "runtimeRole", "hub", "client", "unauthenticatedLoopbackListener"]);
4
+ const baselines = new WeakMap<OcxConfig, Record<string, unknown>>();
5
+
6
+ function snapshot(config: OcxConfig): Record<string, unknown> {
7
+ const record = config as unknown as Record<string, unknown>;
8
+ return Object.fromEntries([...LIVE_TOPOLOGY_KEYS].filter(key => Object.hasOwn(record, key))
9
+ .map(key => [key, structuredClone(record[key])]));
10
+ }
11
+
12
+ /** Track actual active values independently of the desired config last persisted. */
13
+ export function captureLiveTopologyBaseline(config: OcxConfig): void {
14
+ baselines.set(config, snapshot(config));
15
+ }
16
+
17
+ /** Merge desired next-start topology without mutating the active listener/role objects. */
18
+ export function projectLiveTopology(
19
+ config: OcxConfig, persisted: OcxConfig,
20
+ mergeRecord: (live: Record<string, unknown>, baseline: Record<string, unknown>, disk: Record<string, unknown>, skipped: ReadonlySet<string>) => void,
21
+ ): OcxConfig {
22
+ const projected = { ...config, ...snapshot(config) } as unknown as Record<string, unknown>;
23
+ const baseline = baselines.get(config) ?? snapshot(config);
24
+ const disk = persisted as unknown as Record<string, unknown>;
25
+ const skipped = new Set([...Object.keys(projected), ...Object.keys(baseline), ...Object.keys(disk)]
26
+ .filter(key => !LIVE_TOPOLOGY_KEYS.has(key)));
27
+ mergeRecord(projected, baseline, disk, skipped);
28
+ return projected as unknown as OcxConfig;
29
+ }
@@ -3,6 +3,7 @@ import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
3
3
  import { chmodSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
6
+ import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
6
7
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
7
8
  import { anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
8
9
  import {
@@ -16,6 +17,7 @@ import {
16
17
  } from "./provider-validation";
17
18
  import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../codex/upstream-host-health";
18
19
  import { hardenSecretPath } from "../lib/windows-secret-acl";
20
+ import { isPassiveRead } from "../lib/passive-read";
19
21
  import { redactSecretString } from "../lib/redact";
20
22
  import { isValidProviderName } from "./provider-name";
21
23
  import { MODEL_ALIAS_PATTERN } from "../providers/default-aliases";
@@ -49,10 +51,13 @@ import {
49
51
  import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
50
52
 
51
53
  export function hardenExistingSecret(path: string): void {
52
- if (existsSync(path)) {
53
- try { chmodSync(path, 0o600); } catch { /* best-effort */ }
54
+ if (existsSync(path) && !isPassiveRead()) {
54
55
  if (process.platform === "win32") {
56
+ // Preserve the ACL helper's metadata observation across reads. Windows chmod does
57
+ // not establish a private DACL and can invalidate that observation before every load.
55
58
  hardenSecretPath(path, { required: false });
59
+ } else {
60
+ try { chmodSync(path, 0o600); } catch { /* best-effort */ }
56
61
  }
57
62
  }
58
63
  }
@@ -128,6 +133,7 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
128
133
  export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
129
134
  setCodexWsReuseAcrossTurns(validated.codexWsReuseAcrossTurns === true);
130
135
  if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
136
+ if (desktopCompatibilityConfigError(rawParsed)) console.warn("Invalid desktopCompatibility startup preference ignored; no desktop compatibility service will start automatically.");
131
137
  if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
132
138
  const chatgptDesktop = chatgptDesktopConfigIssue(rawParsed);
133
139
  if (chatgptDesktop) console.warn(`⚠️ config.json ${chatgptDesktop} — the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { flushWindowsSecretAclReapsBeforeRemoval, hardenSecretDirAsync, windowsSecretAclApplies } from "../lib/windows-secret-acl";
5
5
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
6
+ import { isPassiveRead } from "../lib/passive-read";
7
+ import { trackBackgroundWork } from "../lib/background-work";
6
8
 
7
9
  /**
8
10
  * Expand a leading `~` in user-supplied paths without interpreting shell
@@ -29,22 +31,30 @@ export function getConfigPath(): string {
29
31
  }
30
32
 
31
33
  export function hardenConfigDir(): void {
34
+ // A passive read (a Remote Link Child's, #6959) repairs nothing on this machine.
35
+ if (isPassiveRead()) return;
32
36
  const dir = getConfigDir();
33
37
  // The guard runs before any mutation: refusing after chmod/ACL would already
34
38
  // have changed the protected directory used by the test-home boundary.
35
39
  assertNotRealHomeUnderTest(dir);
36
40
  if (!existsSync(dir)) return;
37
- try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
41
+ // Windows privacy is enforced by NTFS ACLs. chmod only changes DOS attributes there;
42
+ // doing it before the ACL lookup can invalidate its ctime-based successful observation.
43
+ if (!windowsSecretAclApplies()) {
44
+ try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
45
+ }
38
46
  if (windowsSecretAclApplies() && !configDirHardeningFlights.has(dir)) {
39
47
  // This is an optional read-path harden. Waiting synchronously here used to stop the Bun
40
48
  // event loop (including /healthz) for the full icacls timeout. Required mutation paths keep
41
49
  // their own awaited/fail-closed hardening; ordinary config reads only start one soft flight.
42
- const flight = hardenSecretDirAsync(dir, { required: false })
50
+ // Tracked: icacls holds the directory open, and a config write or removal that lands on
51
+ // it before the child exits fails with a sharing violation on Windows.
52
+ const flight = trackBackgroundWork("config-dir-acl-harden", hardenSecretDirAsync(dir, { required: false })
43
53
  .then(() => undefined)
44
54
  .catch(() => undefined)
45
55
  .finally(() => {
46
56
  if (configDirHardeningFlights.get(dir) === flight) configDirHardeningFlights.delete(dir);
47
- });
57
+ }));
48
58
  configDirHardeningFlights.set(dir, flight);
49
59
  }
50
60
  }
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import * as z from "zod/v4";
4
4
  import { CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR } from "../codex/account-namespace-match";
5
5
  import { redactSecretString } from "../lib/redact";
6
+ import { isPassiveRead } from "../lib/passive-read";
6
7
  import { hasWarnedConfigFallback, markWarnedConfigFallback } from "./warn-memo";
7
8
  import { configSchema } from "./schema/config-schema";
8
9
  import type { OcxConfig } from "../types";
@@ -232,7 +233,8 @@ export function warnAndBackupInvalidConfig(configPath: string, error: unknown):
232
233
  }
233
234
 
234
235
  export function backupInvalidConfig(configPath: string): string | null {
235
- if (!existsSync(configPath)) return null;
236
+ // A passive read leaves a malformed file for the Home's own load to back up (#6959).
237
+ if (!existsSync(configPath) || isPassiveRead()) return null;
236
238
  const backupPath = `${configPath}.invalid-${new Date().toISOString().replace(/[:.]/g, "-")}`;
237
239
  try {
238
240
  copyFileSync(configPath, backupPath);
@@ -2,6 +2,7 @@ import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
2
2
  import * as z from "zod/v4";
3
3
  import { anthropicAccountPoolSchema } from "./anthropic-account-pool";
4
4
  import { compactionRecoverySchema } from "./compaction-recovery";
5
+ import { desktopCompatibilitySchema } from "./desktop-compatibility";
5
6
  import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
6
7
  import {
7
8
  agentTaskRecoverySchema,
@@ -83,6 +84,7 @@ export const protocolConfigSchema = z.object({
83
84
  managedMessagesNative: z.boolean().optional(),
84
85
  managedMessagesNativeOAuth: z.boolean().optional(),
85
86
  directEncoders: z.boolean().optional(),
87
+ antigravityDirect: z.boolean().optional(),
86
88
  shadowPlan: z.boolean().optional(),
87
89
  }).strict().optional(),
88
90
  }).strict();
@@ -105,6 +107,8 @@ export const configSchema = z.object({
105
107
  skills: skillsConfigSchema.optional().catch(undefined),
106
108
  // Malformed hand edits disable this opt-in exporter. Live writes reject them in diagnostics.ts.
107
109
  metricsExport: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
110
+ // Same contract as metricsExport: a malformed hand edit leaves recovery off; live writes reject it.
111
+ serviceHealthRecovery: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
108
112
  // Kept raw on purpose: `.catch(undefined)` would turn a mistyped `enabled` into "inherit",
109
113
  // which can reopen a surface the operator meant to close. src/protocols/settings.ts parses it
110
114
  // and fails closed instead.
@@ -122,6 +126,7 @@ export const configSchema = z.object({
122
126
  managedMessagesNative: false,
123
127
  managedMessagesNativeOAuth: false,
124
128
  directEncoders: fields.directEncoders === true,
129
+ antigravityDirect: fields.antigravityDirect === true,
125
130
  shadowPlan: fields.shadowPlan === true,
126
131
  },
127
132
  };
@@ -188,6 +193,7 @@ export const configSchema = z.object({
188
193
  modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
189
194
  compactionRouting: compactionRoutingSchema.optional().catch(undefined),
190
195
  compactionRecovery: compactionRecoverySchema.optional().catch(undefined),
196
+ desktopCompatibility: desktopCompatibilitySchema.optional().catch(undefined),
191
197
  // A hand-edited malformed phase disables only that phase instead of rejecting
192
198
  // providers/apiKeys, matching the load-time degradation notice; the management write
193
199
  // boundary (validateConfigCandidate) still refuses the bad value through the shared,
@@ -215,6 +221,8 @@ export const configSchema = z.object({
215
221
  short: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
216
222
  long: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
217
223
  }).optional().catch(undefined),
224
+ // #2999: a malformed hand edit falls back to auto detection rather than rejecting the config.
225
+ nativeMainRefresh: z.enum(["codex", "opencodex"]).optional().catch(undefined),
218
226
  // Future versions remain opaque through passthrough-compatible whole-config saves.
219
227
  // Only version 1 grants deletion authority in the rebase path.
220
228
  configRebaseProvenance: z.unknown().optional(),
@@ -676,6 +684,17 @@ export const configSchema = z.object({
676
684
  message: reasoningSummaryDeliveryError,
677
685
  });
678
686
  }
687
+ const minMaxOutputError = positiveIntegerConfigError(
688
+ (provider as { minMaxOutputTokens?: unknown }).minMaxOutputTokens,
689
+ "minMaxOutputTokens",
690
+ );
691
+ if (minMaxOutputError) {
692
+ ctx.addIssue({
693
+ code: "custom",
694
+ path: ["providers", redactSecretString(name), "minMaxOutputTokens"],
695
+ message: minMaxOutputError,
696
+ });
697
+ }
679
698
  const defaultMaxOutputError = positiveIntegerConfigError(
680
699
  (provider as { defaultMaxOutputTokens?: unknown }).defaultMaxOutputTokens,
681
700
  "defaultMaxOutputTokens",
@@ -0,0 +1,9 @@
1
+ import * as z from "zod/v4";
2
+ /** Startup resumes observation only. Certificate trust, app launch and Apply are never saved here. */
3
+ export const desktopCompatibilitySchema = z.object({ startOnProxyStart: z.boolean() }).strict();
4
+ export function desktopCompatibilityConfigError(value: unknown): string | null {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
6
+ const setting = (value as Record<string, unknown>).desktopCompatibility;
7
+ return setting === undefined || desktopCompatibilitySchema.safeParse(setting).success
8
+ ? null : "schema_invalid: desktopCompatibility: requires only boolean startOnProxyStart";
9
+ }
@@ -331,6 +331,7 @@ export const providerConfigSchema = z.object({
331
331
  responseTierAuthoritative: z.boolean().optional(),
332
332
  fastEnabled: z.boolean().optional(),
333
333
  supportsServiceTier: z.boolean().optional(),
334
+ inferContextWindow: z.boolean().optional(),
334
335
  modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
335
336
  modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
336
337
  preserveResponsesReasoningContent: z.boolean().optional(),