@bitkyc08/opencodex 2.84.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
package/src/bridge/sse.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { anthropicProgressItems } from "../responses/anthropic-progress";
1
2
  import type {
2
3
  AdapterEvent,
3
4
  OcxMessagePhase,
@@ -12,7 +13,7 @@ import {
12
13
  classifyError,
13
14
  cyberPolicyErrorType,
14
15
  CYBER_POLICY_ERROR_CODE,
15
- isCyberPolicyCode,
16
+ isTerminalRefusalCode,
16
17
  type OcxErrorPayload,
17
18
  } from "../lib/errors";
18
19
  import { redactSecretString } from "../lib/redact";
@@ -50,6 +51,7 @@ import {
50
51
  } from "../lib/translator-budget";
51
52
  import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
52
53
  import type { OutputItem, StringChunks } from "./internal";
54
+ import { terminalRefusalFailure } from "./terminal-refusal";
53
55
 
54
56
  function sseEvent(name: string, data: Record<string, unknown>): string {
55
57
  return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
@@ -88,6 +90,7 @@ export function bridgeToResponsesSSE(
88
90
  * response.completed — codex-rs collect_compaction_output requires exactly one.
89
91
  */
90
92
  compaction?: boolean;
93
+ compactionRetention?: { encode(summary: string): string; commit(): void };
91
94
  /** One-shot: first non-empty text/thinking/raw-reasoning/tool-input delta observed. */
92
95
  onFirstOutput?: () => void;
93
96
  onTerminal?: (status: ResponsesTerminalStatus) => void;
@@ -120,6 +123,12 @@ export function bridgeToResponsesSSE(
120
123
  * would reach the client under the invented name instead of the normalized one.
121
124
  */
122
125
  enforceDeclaredToolNames?: boolean;
126
+ /**
127
+ * Deliver a non-retryable upstream refusal as `response.failed` with a terminal-refusal code
128
+ * instead of `response.incomplete` (see `./terminal-refusal`). Set only for the Responses
129
+ * inbound wire: the chat and Anthropic encoders read the incomplete to report a filter stop.
130
+ */
131
+ terminalRefusalAsFailed?: boolean;
123
132
  /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
124
133
  toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
125
134
  /**
@@ -676,12 +685,15 @@ export function bridgeToResponsesSSE(
676
685
  // leaves a "Searching the web" spinner spinning forever.
677
686
  // `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
678
687
  // downstream translators (claude outbound) can fill web_search_tool_result content.
679
- const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
688
+ // `fetch` (Anthropic's server-side web_fetch) makes the cell a page open and rides its result
689
+ // verbatim as `fetch_result`, for the same claude outbound translator.
690
+ const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], fetch?: { url: string; result: unknown }) => {
680
691
  if (!currentWebSearch) return;
681
692
  const item = {
682
693
  type: "web_search_call", id: currentWebSearch.itemId, status,
683
- action: webSearchAction(queries),
694
+ action: webSearchAction(queries, fetch),
684
695
  ...(sources && sources.length > 0 ? { sources } : {}),
696
+ ...(fetch ? { fetch_result: fetch.result } : {}),
685
697
  };
686
698
  emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
687
699
  retainFinishedItem(item as OutputItem);
@@ -841,6 +853,7 @@ export function bridgeToResponsesSSE(
841
853
  // its compaction UI renders nothing mid-turn, so nothing is lost visually.
842
854
  if (options?.compaction) {
843
855
  if (event.type === "text_delta") {
856
+ if (event.anthropicThinkingSignature) continue;
844
857
  compaction = appendString(
845
858
  compaction,
846
859
  event.text,
@@ -871,6 +884,29 @@ export function bridgeToResponsesSSE(
871
884
  break;
872
885
  }
873
886
  case "text_delta": {
887
+ if (event.anthropicThinkingSignature) {
888
+ rawReasoningForNextToolCall = "";
889
+ closeCurrentMessage("commentary");
890
+ closeCurrentReasoning();
891
+ closeCurrentRawReasoning();
892
+ flushHiddenRawReasoning();
893
+ flushHiddenReasoningEnvelope();
894
+ if (currentToolCall) closeCurrentToolCall();
895
+ for (const item of anthropicProgressItems(event.text, event.anthropicThinkingSignature, budget)) {
896
+ emit("response.output_item.added", { output_index: outputIndex, item: { ...item, ...(item.type === "message" ? { status: "in_progress", content: [] } : {}) } });
897
+ if (item.type === "message") {
898
+ const part = item.content![0];
899
+ emit("response.content_part.added", { item_id: item.id, output_index: outputIndex, content_index: 0, part: { ...part, text: "" } });
900
+ emit("response.output_text.delta", { item_id: item.id, output_index: outputIndex, content_index: 0, delta: event.text });
901
+ emit("response.output_text.done", { item_id: item.id, output_index: outputIndex, content_index: 0, text: event.text });
902
+ emit("response.content_part.done", { item_id: item.id, output_index: outputIndex, content_index: 0, part });
903
+ }
904
+ emit("response.output_item.done", { output_index: outputIndex, item });
905
+ retainFinishedItem(item as OutputItem);
906
+ outputIndex++;
907
+ }
908
+ break;
909
+ }
874
910
  if (currentReasoning) closeCurrentReasoning();
875
911
  if (currentRawReasoning) closeCurrentRawReasoning();
876
912
  flushHiddenRawReasoning();
@@ -1195,7 +1231,7 @@ export function bridgeToResponsesSSE(
1195
1231
  currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
1196
1232
  }
1197
1233
  const safeSources = safeWebSearchSources(event.sources);
1198
- closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
1234
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.fetch);
1199
1235
  // Queue this search's sources for the next assistant message (dedup by URL).
1200
1236
  if (safeSources.length > 0) {
1201
1237
  for (const source of safeSources) {
@@ -1233,7 +1269,7 @@ export function bridgeToResponsesSSE(
1233
1269
  // Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
1234
1270
  const item = {
1235
1271
  type: "compaction", id: `cmp_${uuid()}`,
1236
- encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
1272
+ encrypted_content: event.compactionEncryptedContent ?? (options?.compactionRetention?.encode(joinChunks(compaction)) ?? encodeCompactionSummary(joinChunks(compaction))),
1237
1273
  };
1238
1274
  emit("response.output_item.done", { output_index: outputIndex, item });
1239
1275
  retainFinishedItem(item as OutputItem, event.compactionEncryptedContent ? 0 : compaction.bytes);
@@ -1271,6 +1307,7 @@ export function bridgeToResponsesSSE(
1271
1307
  emit("response.completed", {
1272
1308
  response,
1273
1309
  });
1310
+ if (!closed && !terminated && options?.compaction && !event.compactionEncryptedContent) options.compactionRetention?.commit();
1274
1311
  reportTerminal("completed");
1275
1312
  }
1276
1313
  terminalEvent = true;
@@ -1287,12 +1324,25 @@ export function bridgeToResponsesSSE(
1287
1324
  flushHiddenReasoningEnvelope();
1288
1325
  options?.onUsage?.(event.usage);
1289
1326
  await awaitThoughtSignatureDurability();
1327
+ const refusal = options?.terminalRefusalAsFailed ? terminalRefusalFailure(event) : undefined;
1328
+ if (refusal) {
1329
+ emit("response.failed", {
1330
+ response: {
1331
+ ...responseSnapshot("failed", finishedItems, event.endTurn),
1332
+ usage: responsesUsage(event.usage),
1333
+ ...refusal,
1334
+ },
1335
+ });
1336
+ reportTerminal("failed");
1337
+ terminalEvent = true;
1338
+ break;
1339
+ }
1290
1340
  emit("response.incomplete", {
1291
1341
  response: {
1292
1342
  ...responseSnapshot("incomplete", finishedItems, event.endTurn),
1293
1343
  usage: responsesUsage(event.usage),
1294
1344
  incomplete_details: {
1295
- reason: event.reason,
1345
+ reason: truncationReasonFor(event.reason) ?? event.reason,
1296
1346
  ...(event.message ? { message: event.message } : {}),
1297
1347
  ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
1298
1348
  },
@@ -1325,7 +1375,7 @@ export function bridgeToResponsesSSE(
1325
1375
  ...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
1326
1376
  error: failure.error,
1327
1377
  last_error: failure.error,
1328
- ...(isCyberPolicyCode(failure.error.code)
1378
+ ...(isTerminalRefusalCode(failure.error.code)
1329
1379
  ? { retryable: false }
1330
1380
  : event.retryable !== undefined ? { retryable: event.retryable } : {}),
1331
1381
  },
@@ -1366,7 +1416,7 @@ export function bridgeToResponsesSSE(
1366
1416
  ...responseSnapshot("failed", finishedItems),
1367
1417
  error: failure,
1368
1418
  last_error: failure,
1369
- ...(isCyberPolicyCode(failure.code) ? { retryable: false } : {}),
1419
+ ...(isTerminalRefusalCode(failure.code) ? { retryable: false } : {}),
1370
1420
  },
1371
1421
  });
1372
1422
  reportTerminal("failed");
@@ -0,0 +1,59 @@
1
+ import type { AdapterEvent } from "../types";
2
+ import type { OcxErrorPayload } from "../lib/errors";
3
+ import { redactSecretString } from "../lib/redact";
4
+
5
+ /**
6
+ * The terminal-refusal code Codex ends a turn on. It is a member of `TERMINAL_REFUSAL_CODES`
7
+ * in `src/lib/errors.ts`, and the one that says "this prompt, as sent, is refused" without
8
+ * claiming a specific policy family the upstream never named.
9
+ */
10
+ const TERMINAL_REFUSAL_FAILED_CODE = "invalid_prompt";
11
+
12
+ interface TerminalRefusalFailure {
13
+ error: OcxErrorPayload;
14
+ last_error: OcxErrorPayload;
15
+ retryable: false;
16
+ }
17
+
18
+ /**
19
+ * Client-visible copy: what happened, and that resending the same context cannot help. It points
20
+ * at a new conversation rather than a new turn, because a later turn replays the refused context.
21
+ */
22
+ function terminalRefusalMessage(detail: string | undefined): string {
23
+ const reason = detail?.trim() ? ` (${redactSecretString(detail.trim())})` : "";
24
+ return `The upstream model refused this request${reason}. `
25
+ + "Retrying with the same conversation context will not succeed; change the request or start a new conversation.";
26
+ }
27
+
28
+ /**
29
+ * An adapter `incomplete` that is a non-retryable upstream refusal: a content filter carrying an
30
+ * explicit `retryable: false` verdict. A filter without the verdict, an output cap, a stall and an
31
+ * adapter EOF do not qualify. The sidecar loops read this too: a refused iteration ends the turn,
32
+ * so a sidecar call it carried is neither run nor answered with another model request.
33
+ */
34
+ export function isTerminalRefusalIncomplete(event: AdapterEvent | undefined): boolean {
35
+ return event?.type === "incomplete" && event.reason === "content_filter" && event.retryable === false;
36
+ }
37
+
38
+ /**
39
+ * `response.failed` fields for an adapter `incomplete` that is a non-retryable upstream refusal
40
+ * (#4312), or `undefined` when the event is anything else. The Anthropic and Kiro adapters are
41
+ * the ones that emit it today.
42
+ *
43
+ * codex-rs maps every `response.incomplete` to a retryable stream error and never reads
44
+ * `incomplete_details.retryable`, so a refusal delivered that way is re-sent through the whole
45
+ * stream retry budget. It only ends the turn on `response.failed` carrying a terminal-refusal
46
+ * `error.code`. Only an explicit `retryable: false` content filter qualifies: a filter without
47
+ * the verdict, an output cap, a stall and an adapter EOF stay `response.incomplete`.
48
+ */
49
+ export function terminalRefusalFailure(
50
+ event: Extract<AdapterEvent, { type: "incomplete" }>,
51
+ ): TerminalRefusalFailure | undefined {
52
+ if (!isTerminalRefusalIncomplete(event)) return undefined;
53
+ const error: OcxErrorPayload = {
54
+ type: "invalid_request_error",
55
+ code: TERMINAL_REFUSAL_FAILED_CODE,
56
+ message: terminalRefusalMessage(event.message),
57
+ };
58
+ return { error, last_error: error, retryable: false };
59
+ }
@@ -36,7 +36,7 @@ export function assertChatCompletionsRoutingBody(raw: unknown): asserts raw is C
36
36
  const OUTPUT_CONFIG_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
37
37
  const OUTPUT_CONFIG_SUMMARIES = new Set(["auto", "concise", "detailed", "none"]);
38
38
 
39
- function contentToText(content: unknown): string {
39
+ export function contentToText(content: unknown): string {
40
40
  if (typeof content === "string") return content;
41
41
  if (!Array.isArray(content)) return "";
42
42
  const parts: string[] = [];
@@ -62,7 +62,7 @@ const imageUrlFromPart = chatImageUrlFromPart;
62
62
  * understanding is requested by (#3271). The object form is the only one that
63
63
  * can carry it — `video_url` as a bare string has nowhere to put it.
64
64
  */
65
- function videoFromPart(part: Rec): { url: string; processing?: string } | null {
65
+ export function videoFromPart(part: Rec): { url: string; processing?: string } | null {
66
66
  if (part.type !== "video_url") return null;
67
67
  const videoUrl = part.video_url;
68
68
  if (typeof videoUrl === "string" && videoUrl.length > 0) return { url: videoUrl };
@@ -123,7 +123,7 @@ function userContentToBlocks(content: unknown): Rec[] {
123
123
  * was dropped in silence (#5212). A part with no inline bytes is still not translatable and is
124
124
  * left to the untranslated-media refusal, which runs before this loop.
125
125
  */
126
- function fileFromPart(part: Rec): Rec | null {
126
+ export function fileFromPart(part: Rec): Rec | null {
127
127
  if (part.type !== "file" && part.type !== "input_file") return null;
128
128
  const file = isRec(part.file) ? part.file : part;
129
129
  const fileData = file.file_data;
@@ -143,7 +143,7 @@ function fileFromPart(part: Rec): Rec | null {
143
143
  * Only representable plaintext is read. No signature, encrypted payload or
144
144
  * provider-issued item id is reconstructed — see the reasoning item built below.
145
145
  */
146
- function assistantReasoningText(msg: Rec): string | undefined {
146
+ export function assistantReasoningText(msg: Rec): string | undefined {
147
147
  if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) {
148
148
  return msg.reasoning_content;
149
149
  }
@@ -601,6 +601,32 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
601
601
  ...(legacyFunctionsToResponses(raw.functions) ?? []),
602
602
  ];
603
603
  if (tools.length > 0) body.tools = tools;
604
+ applyChatSettings(raw, body);
605
+ return body;
606
+ }
607
+
608
+ /**
609
+ * The request settings of a Chat body as a Responses body with no conversation: `input` is
610
+ * empty and there are no `instructions` or `tools`. The Antigravity direct lane hands this to
611
+ * the Responses core so routing, effort policy, tier and logging keep reading the fields they
612
+ * read today, while the conversation is decoded straight into the IR
613
+ * (`src/protocols/codecs/chat-ir.ts`). Field order and validation are the bridge body's own:
614
+ * both are built by `applyChatSettings`.
615
+ */
616
+ export function chatSettingsToResponsesBody(raw: unknown): Rec {
617
+ assertChatCompletionsRoutingBody(raw);
618
+ const body: Rec = {
619
+ model: raw.model,
620
+ input: [],
621
+ stream: raw.stream === true,
622
+ store: false,
623
+ };
624
+ applyChatSettings(raw, body);
625
+ return body;
626
+ }
627
+
628
+ /** Everything after the conversation and tool catalog: choice, limits, sampling, reasoning, format. */
629
+ function applyChatSettings(raw: ChatCompletionsRoutingBody, body: Rec): void {
604
630
  if (raw.tool_choice !== undefined) toolChoiceToResponses(raw.tool_choice, body);
605
631
  else legacyFunctionChoiceToResponses(raw.function_call, body);
606
632
 
@@ -637,6 +663,4 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
637
663
 
638
664
  const text = responseFormatToText(raw.response_format);
639
665
  if (text) body.text = text;
640
-
641
- return body;
642
666
  }
@@ -19,6 +19,7 @@ import {
19
19
  CYBER_POLICY_ERROR_CODE,
20
20
  isCyberPolicyCode,
21
21
  isCyberPolicyMessage,
22
+ UNDECLARED_TOOL_CALL_ERROR_CODE,
22
23
  } from "../lib/errors";
23
24
  import { redactSecretString } from "../lib/redact";
24
25
  import { createSseBlockBuffer, sseDataPayload } from "../server/sse-payload-rewrite";
@@ -196,6 +197,10 @@ export function chatCompletionsStreamErrorPayload(
196
197
  } else if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
197
198
  classified.code = CYBER_POLICY_ERROR_CODE;
198
199
  classified.type = cyberPolicyErrorType(details?.type);
200
+ } else if (details?.code === UNDECLARED_TOOL_CALL_ERROR_CODE) {
201
+ // The guard's deterministic refusal keeps its wire identity past classifyError's
202
+ // generic 5xx remap — same code the buffered Chat error and the SSE frame carry.
203
+ classified.code = UNDECLARED_TOOL_CALL_ERROR_CODE;
199
204
  } else if (details?.code !== undefined && details.code !== null && !classified.code) {
200
205
  classified.code = details.code;
201
206
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, type BigIntStats } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
@@ -44,9 +44,60 @@ export interface Desktop3pMetadata {
44
44
 
45
45
  export const DESKTOP_PICKER_ENTRY_NAME = "opencodex-picker";
46
46
 
47
+ /** A path that no longer names the observed file was swapped; a same-path failure keeps its own errno. */
48
+ function pathStillNamesExpectedFile(path: string, expected: BigIntStats): boolean {
49
+ try {
50
+ const now = lstatSync(path, { bigint: true });
51
+ return now.isFile() && now.dev === expected.dev && now.ino === expected.ino
52
+ && now.ctimeNs === expected.ctimeNs;
53
+ } catch { return false; }
54
+ }
55
+
56
+ /** Read regular config files only; a path swap must not turn an observation into a FIFO wait. */
57
+ export function readDesktopLibraryBytes(path: string): Buffer {
58
+ const expected = lstatSync(path, { bigint: true });
59
+ if (!expected.isFile()) throw new Error("desktop_config_not_regular");
60
+ // POSIX open of a FIFO can itself block, before fstat is possible. Windows has no POSIX
61
+ // FIFO entry; retain descriptor identity validation there without unsupported open flags.
62
+ const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NONBLOCK | constants.O_NOFOLLOW);
63
+ let fd: number;
64
+ try {
65
+ fd = openSync(path, flags);
66
+ } catch (error) {
67
+ // A swap can fail at open — ELOOP on a symlink, ENXIO on a socket — before fstat can
68
+ // classify it. Re-observing the path reports every swap as the documented refusal.
69
+ if (pathStillNamesExpectedFile(path, expected)) throw error;
70
+ throw new Error("desktop_config_not_regular");
71
+ }
72
+ try {
73
+ const opened = fstatSync(fd, { bigint: true });
74
+ if (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino
75
+ || opened.ctimeNs !== expected.ctimeNs) throw new Error("desktop_config_not_regular");
76
+ return readFileSync(fd);
77
+ } finally {
78
+ closeSync(fd);
79
+ }
80
+ }
81
+
82
+ /** Text decoding stays identical to node:fs readFileSync(path, "utf8"). */
83
+ export function readDesktopLibraryText(path: string): string {
84
+ return readDesktopLibraryBytes(path).toString("utf8");
85
+ }
86
+
87
+ /** Only an absent directory entry is optional; dangling links and failed reads still refuse. */
88
+ export function readOptionalDesktopLibraryBytes(path: string): Buffer | undefined {
89
+ try { return readDesktopLibraryBytes(path); }
90
+ catch (error) {
91
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /** Read optional verified metadata; a present file must contain an entries array. */
47
97
  export function parseMetadata(path: string): Desktop3pMetadata {
48
- if (!existsSync(path)) return { entries: [] };
49
- const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<Desktop3pMetadata>;
98
+ const bytes = readOptionalDesktopLibraryBytes(path);
99
+ if (bytes === undefined) return { entries: [] };
100
+ const parsed = JSON.parse(bytes.toString("utf8")) as Partial<Desktop3pMetadata>;
50
101
  if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
51
102
  return { ...parsed, entries: parsed.entries };
52
103
  }
@@ -80,11 +131,25 @@ export const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
80
131
  "inferenceModels",
81
132
  ]);
82
133
 
83
- export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
84
- if (!existsSync(path)) return {};
85
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
134
+ export interface DesktopProfileSnapshot {
135
+ /** Verified bytes of the existing profile; undefined only when the entry is absent. */
136
+ bytes?: Buffer;
137
+ foreign: Record<string, unknown>;
138
+ }
139
+
140
+ /** Capture verified bytes for key preservation, backup and rollback; publication revalidates them. */
141
+ export function readDesktopProfileSnapshot(path: string): DesktopProfileSnapshot {
142
+ const bytes = readOptionalDesktopLibraryBytes(path);
143
+ if (bytes === undefined) return { foreign: {} };
144
+ const parsed = JSON.parse(bytes.toString("utf8")) as unknown;
86
145
  if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
87
- return Object.fromEntries(
146
+ const foreign = Object.fromEntries(
88
147
  Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
89
148
  );
149
+ return { bytes, foreign };
150
+ }
151
+
152
+ /** Preserve settings outside the gateway-owned keys from a verified profile. */
153
+ export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
154
+ return readDesktopProfileSnapshot(path).foreign;
90
155
  }
@@ -5,21 +5,23 @@ import { applyRemoteDesktopStore, inspectRemoteDesktopCleanup, restoreRemoteDesk
5
5
  import { canonicalDirectory } from "./desktop-remote-store-io";
6
6
  import {
7
7
  resolveDesktop3pConfigLibraryPath, parseMetadata, SAFE_DESKTOP_PROFILE_ID, isRecord,
8
- isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileForeignKeys,
8
+ isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileSnapshot, readDesktopLibraryText, readDesktopLibraryBytes,
9
+ readOptionalDesktopLibraryBytes,
9
10
  type Desktop3pConfigLibraryOptions, type Desktop3pMetadata, type Desktop3pMetadataEntry,
10
11
  } from "./desktop-3p-library";
11
12
  export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
12
13
  import { createHash, randomUUID } from "node:crypto";
13
- import { copyFileSync, existsSync, mkdirSync, readFileSync, lstatSync, unlinkSync } from "node:fs";
14
+ import { existsSync, mkdirSync, lstatSync, unlinkSync, writeFileSync } from "node:fs";
14
15
  import { join } from "node:path";
15
16
  import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
16
- import { atomicWriteFileNoFollow } from "../config/atomic-write";
17
+ import { atomicWriteFileNoFollow, atomicWriteFileStreamed } from "../config/atomic-write";
17
18
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
18
19
  import type { OcxClaudeDesktopProfile } from "../types";
19
20
  import {
20
21
  reconcileDesktopProfile,
21
22
  renderDesktopProfile,
22
23
  validDateAlias,
24
+ legacyDesktopProfileWireAlias,
23
25
  type DesktopProfileModel,
24
26
  } from "./desktop-profile";
25
27
  import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
@@ -223,7 +225,7 @@ function collectDesktop3pModels(
223
225
  ...(model.supports1m ? { supports1m: true, prefer1m: true } : {}),
224
226
  });
225
227
  }
226
- // Hash aliases from both pre-profile generations are compatibility-only and can
228
+ // Previous profile IDs and both hash generations are compatibility-only and can
227
229
  // collide. Bind them in stable route order so changing a family default or
228
230
  // rendered ordering can never silently rebind an old Desktop id.
229
231
  for (const model of [...rendered].sort((a, b) => a.route.localeCompare(b.route))) {
@@ -231,7 +233,8 @@ function collectDesktop3pModels(
231
233
  const providerEnd = model.route.indexOf("/");
232
234
  const provider = model.route.slice(0, providerEnd);
233
235
  const id = model.route.slice(providerEnd + 1);
234
- for (const legacy of [desktop3pAlias(provider, id), legacyDesktop3pAlias(provider, id)]) {
236
+ const storedAlias = reconciled.assignments[model.route]!.alias;
237
+ for (const legacy of [legacyDesktopProfileWireAlias(storedAlias), desktop3pAlias(provider, id), legacyDesktop3pAlias(provider, id)]) {
235
238
  if (realAnthropicIds.has(legacy)) {
236
239
  console.warn(`[opencodex] Claude Desktop legacy alias ${legacy} for ${model.route} conflicts with a real Anthropic model; ignoring the alias`);
237
240
  continue;
@@ -360,6 +363,11 @@ export function isKnownDesktop3pModelId(id: string): boolean {
360
363
  return desktop3pRegistry.has(id) || desktop3pRealAnthropicIds.has(id);
361
364
  }
362
365
 
366
+ /** Real catalog IDs retain identity even when their spelling resembles an alias. */
367
+ export function isRealDesktop3pModelId(id: string): boolean {
368
+ return desktop3pRealAnthropicIds.has(id);
369
+ }
370
+
363
371
  /** Only missing IDs in the emitted Desktop namespaces are managed-alias errors. */
364
372
  export function isUnresolvedDesktop3pAlias(id: string): boolean {
365
373
  if (isKnownDesktop3pModelId(id)) return false;
@@ -369,7 +377,7 @@ export function isUnresolvedDesktop3pAlias(id: string): boolean {
369
377
  if (isKnownDesktop3pModelId(base)) return false;
370
378
  return validDateAlias(base)
371
379
  || /^claude-opus-4-(?:8-)?[a-z][a-z0-9]{2}$/.test(base)
372
- || /^claude-opus-4-8-p[0-9a-z]{3}$/.test(base);
380
+ || /^claude-(?:opus-4-8|fable-5)-p[0-9a-z]{3}$/.test(base);
373
381
  }
374
382
 
375
383
  /** Alias selected by the installed profile registry, falling back to the legacy hash shape. */
@@ -377,6 +385,13 @@ export function activeDesktop3pAlias(provider: string, modelId: string): string
377
385
  return desktop3pAliasesByRoute.get(`${provider}/${modelId}`) ?? desktop3pAlias(provider, modelId);
378
386
  }
379
387
 
388
+ /** Discovery cannot offer a skipped alias or one owned by a different route. */
389
+ export function resolvableDesktop3pAlias(provider: string, modelId: string): string | undefined {
390
+ if (provider === "anthropic" && modelId.startsWith("claude-")) return modelId;
391
+ const alias = activeDesktop3pAlias(provider, modelId);
392
+ return resolveDesktop3pAlias(alias) === `${provider}/${modelId}` ? alias : undefined;
393
+ }
394
+
380
395
  /**
381
396
  * Generate the complete Claude Desktop 3P gateway config.
382
397
  *
@@ -476,7 +491,7 @@ export function inspectDesktop3pConfigLibrary(
476
491
  let profile: Record<string, unknown>;
477
492
  let fingerprint: string;
478
493
  try {
479
- const source = readFileSync(selectedProfilePath, "utf8");
494
+ const source = readDesktopLibraryText(selectedProfilePath);
480
495
  const parsed = JSON.parse(source) as unknown;
481
496
  if (!isRecord(parsed)) return { kind: "broken", libraryPath, selectedProfilePath, appliedId, residualPaths, ownedProfileActive };
482
497
  profile = parsed;
@@ -709,7 +724,7 @@ export function writeDesktop3pConfig(
709
724
  const metadata = parseMetadata(join(inspected.libraryPath, "_meta.json"));
710
725
  if (!isOwnedDesktopGatewayEntry(metadata.entries.find(entry => entry.id === inspected.appliedId))) return refused;
711
726
  try {
712
- const selected = JSON.parse(readFileSync(inspected.selectedProfilePath, "utf8")) as Record<string, unknown>;
727
+ const selected = JSON.parse(readDesktopLibraryText(inspected.selectedProfilePath)) as Record<string, unknown>;
713
728
  mode = selected.modelDiscoveryEnabled !== true ? "static"
714
729
  : Array.isArray(selected.inferenceModels) ? "hybrid" : "discovery";
715
730
  } catch { return refused; }
@@ -786,6 +801,7 @@ function desktopRefreshPathsSafe(libraryPath: string, configPath?: string): bool
786
801
  } catch { return false; }
787
802
  }
788
803
 
804
+ /** Publish a gateway profile from a verified snapshot, rolling it back if metadata fails. */
789
805
  function writeDesktop3pConfigWithGenerator(
790
806
  generate: () => object,
791
807
  refreshOnly?: { requiredId: string; expectedFingerprint: string; admit: () => boolean },
@@ -814,8 +830,8 @@ function writeDesktop3pConfigWithGenerator(
814
830
  : [...metadata.entries, entry];
815
831
 
816
832
  const generated = generate();
817
- const preserved = readDesktopProfileForeignKeys(configPath);
818
- const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
833
+ const previous = readDesktopProfileSnapshot(configPath);
834
+ const configJson = JSON.stringify({ ...previous.foreign, ...generated }, null, 2) + "\n";
819
835
  const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
820
836
  if (refreshOnly) {
821
837
  atomicWriteFileNoFollow(configPath, configJson, undefined, {
@@ -825,7 +841,7 @@ function writeDesktop3pConfigWithGenerator(
825
841
  const freshMetadata = parseMetadata(metadataPath);
826
842
  if (freshMetadata.appliedId !== refreshOnly.requiredId
827
843
  || !isOwnedDesktopGatewayEntry(freshMetadata.entries.find(entry => entry.id === refreshOnly.requiredId))
828
- || createHash("sha256").update(readFileSync(configPath)).digest("hex").slice(0, 16) !== refreshOnly.expectedFingerprint) {
844
+ || createHash("sha256").update(readDesktopLibraryBytes(configPath)).digest("hex").slice(0, 16) !== refreshOnly.expectedFingerprint) {
829
845
  throw new Error();
830
846
  }
831
847
  } catch { throw new Error("desktop_refresh_only_skipped"); }
@@ -833,12 +849,31 @@ function writeDesktop3pConfigWithGenerator(
833
849
  });
834
850
  return { written: true, path: configPath, fingerprint };
835
851
  }
836
- const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
852
+ atomicReplaceDesktopConfig(configPath, configJson, previous.bytes);
837
853
  try {
838
854
  atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
839
855
  } catch (metaError) {
840
- // Rollback: restore the backed-up config if metadata write fails.
841
- if (backupPath && existsSync(backupPath)) copyFileSync(backupPath, configPath);
856
+ // Roll back from the verified bytes in memory: reopening the backup could meet a swapped
857
+ // entry and leave the new profile beside the old metadata. A profile this call created is
858
+ // unreferenced once metadata failed, so remove it rather than orphan one per retry.
859
+ // Rollback is ownership-aware: it acts only while the live profile still holds the bytes
860
+ // this call published, checked by the same comparison that guarded publication.
861
+ const base = metaError instanceof Error ? metaError.message : String(metaError);
862
+ const published = Buffer.from(configJson);
863
+ let owned = false;
864
+ const stillOwned = () => { owned = false; assertDesktopProfileUnchanged(configPath, published); owned = true; };
865
+ try {
866
+ stillOwned();
867
+ if (previous.bytes !== undefined) writeDesktopConfigBytes(configPath, previous.bytes, stillOwned);
868
+ else unlinkSync(configPath);
869
+ } catch (rollbackError) {
870
+ if (!owned) {
871
+ // Another writer replaced the profile after publication; its bytes win over our rollback.
872
+ throw new Error(`${base} (profile rollback skipped: profile changed after publish)`, { cause: metaError });
873
+ }
874
+ const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
875
+ throw new AggregateError([metaError, rollbackError], `${base} (profile rollback also failed: ${detail})`, { cause: metaError });
876
+ }
842
877
  throw metaError;
843
878
  }
844
879
  return { written: true, path: configPath, fingerprint };
@@ -848,14 +883,35 @@ function writeDesktop3pConfigWithGenerator(
848
883
  }
849
884
  }
850
885
 
851
- /** Backup an existing owned config then atomically replace it. Exported for failure-path tests. */
886
+ /**
887
+ * Compare the live profile with the caller's verified bytes before backup and before replacement.
888
+ * Refuse drift without retrying; backup and rollback continue to use the original verified bytes.
889
+ * `previous` is undefined only for an absent profile, which leaves any older backup alone.
890
+ * Exported for failure-path tests.
891
+ */
852
892
  export function atomicReplaceDesktopConfig(
853
893
  path: string,
854
894
  content: string,
855
- writer: (path: string, content: string) => void = atomicWriteFile,
895
+ previous: Buffer | undefined,
896
+ writer: typeof atomicWriteFileNoFollow = atomicWriteFileNoFollow,
856
897
  ): { backupPath?: string } {
898
+ const validateBeforeRename = () => assertDesktopProfileUnchanged(path, previous);
899
+ validateBeforeRename();
857
900
  const backupPath = `${path}.bak`;
858
- if (existsSync(path)) copyFileSync(path, backupPath);
859
- writer(path, content);
860
- return existsSync(backupPath) ? { backupPath } : {};
901
+ if (previous !== undefined) writeDesktopConfigBytes(backupPath, previous);
902
+ writer(path, content, undefined, { validateBeforeRename });
903
+ return previous === undefined ? {} : { backupPath };
904
+ }
905
+
906
+ /** Single compare-and-swap check: the live profile must still hold `expected` (undefined = absent). */
907
+ function assertDesktopProfileUnchanged(path: string, expected: Buffer | undefined): void {
908
+ const current = readOptionalDesktopLibraryBytes(path);
909
+ if (expected === undefined ? current !== undefined : current === undefined || !expected.equals(current)) {
910
+ throw new Error("desktop_profile_changed");
911
+ }
912
+ }
913
+
914
+ /** Publish the verified bytes without reopening the source or decoding its backup contents. */
915
+ function writeDesktopConfigBytes(path: string, bytes: Buffer, validateBeforeRename?: () => void): void {
916
+ atomicWriteFileStreamed(path, descriptor => { writeFileSync(descriptor, bytes); }, validateBeforeRename ? { validateBeforeRename } : {});
861
917
  }
@@ -25,7 +25,7 @@ import type { OcxConfig } from "../types";
25
25
  import { claudeConfigDir } from "./auth-detect";
26
26
  import { inspectDesktop3pConfigLibrary } from "./desktop-3p";
27
27
  import { claudeInterceptCaCertPath, ensureLocalInterceptCa } from "./intercept/local-ca";
28
- import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/runtime";
28
+ import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/port-policy";
29
29
  import { ensureClaudeInterceptProxyToken, readClaudeInterceptProxyToken } from "./intercept/proxy-auth";
30
30
  import {
31
31
  applyClaudeInterceptSettings,
@@ -0,0 +1,4 @@
1
+ /** Platforms with a Desktop egress profile and an OS trust adapter for the picker. */
2
+ export function supportsDesktopPicker(platform: NodeJS.Platform = process.platform): boolean {
3
+ return platform === "darwin" || platform === "win32";
4
+ }