@bitkyc08/opencodex 2.84.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * GET / PUT /api/advisor/settings — the advisor sidecar's management surface.
3
+ *
4
+ * GET answers the RESOLVED settings (defaults applied, sources marked) plus availability, so the
5
+ * GUI/CLI show real runtime state rather than stored fields. PUT is a strict partial patch:
6
+ * unknown keys and wrong types are refused, values are validated against the same resolver the
7
+ * runtime reads, and the patch persists through the locked config writer with the snapshot-
8
+ * restore discipline used by PATCH /api/protocols/settings — a refused or failed write never
9
+ * leaves the live config serving a state the file does not hold.
10
+ */
11
+ import { activateAdvisor } from "../../lib/advisor-activation";
12
+ import { jsonResponse } from "../auth-cors";
13
+ import { readManagementJsonBodyOr } from "./body";
14
+ import type { ManagementContext } from "./context";
15
+ import {
16
+ ADVISOR_CONTEXT_SHARING_CONSENT_VERSION,
17
+ ADVISOR_EFFORTS,
18
+ advisorContextSharingBlocked,
19
+ advisorRunnable,
20
+ isCurrentAdvisorContextSharingConsent,
21
+ isValidAdvisorEffort,
22
+ isValidAdvisorPolicy,
23
+ isValidAdvisorTaskLimit,
24
+ resolveAdvisorSettings,
25
+ type AdvisorEffort,
26
+ type AdvisorPolicy,
27
+ } from "../../advisor/settings";
28
+ import { advisorActivityStore } from "../../advisor/activity";
29
+
30
+ const INVALID_BODY = Symbol("invalid-body");
31
+
32
+ interface AdvisorPatch {
33
+ enabled?: boolean;
34
+ model?: string;
35
+ effort?: AdvisorEffort;
36
+ policy?: AdvisorPolicy;
37
+ timeoutMs?: number;
38
+ maxConsultationsPerTask?: number;
39
+ /** "v1" records current consent. null removes it. Enabling does not imply this field. */
40
+ contextSharingConsent?: typeof ADVISOR_CONTEXT_SHARING_CONSENT_VERSION | null;
41
+ reset?: boolean;
42
+ }
43
+
44
+ type ParsedPatch = { ok: true; patch: AdvisorPatch } | { ok: false; code: string; message: string };
45
+
46
+ const PATCH_KEYS = new Set(["enabled", "model", "effort", "policy", "timeoutMs", "maxConsultationsPerTask", "contextSharingConsent", "reset"]);
47
+
48
+ type Rec = Record<string, unknown>;
49
+ function isRec(value: unknown): value is Rec {
50
+ return !!value && typeof value === "object" && !Array.isArray(value);
51
+ }
52
+
53
+ /** Strict: unknown keys and wrong types are refused; messages name the field, never the value. */
54
+ export function parseAdvisorSettingsPatch(body: unknown): ParsedPatch {
55
+ if (!isRec(body)) return { ok: false, code: "invalid_body", message: "body must be a JSON object" };
56
+ if (Object.keys(body).length === 0) return { ok: false, code: "empty_body", message: "body must set at least one supported advisor setting or reset" };
57
+ for (const key of Object.keys(body)) {
58
+ if (!PATCH_KEYS.has(key)) return { ok: false, code: "unknown_field", message: "body accepts only enabled, model, effort, policy, timeoutMs, maxConsultationsPerTask, contextSharingConsent and reset" };
59
+ }
60
+ const patch: AdvisorPatch = {};
61
+ if (body.reset !== undefined) {
62
+ if (body.reset !== true) return { ok: false, code: "invalid_reset", message: "reset must be true when present" };
63
+ if (Object.keys(body).length > 1) return { ok: false, code: "reset_with_fields", message: "reset must be the only field when present" };
64
+ patch.reset = true;
65
+ return { ok: true, patch };
66
+ }
67
+ if (body.enabled !== undefined) {
68
+ if (typeof body.enabled !== "boolean") return { ok: false, code: "invalid_enabled", message: "enabled must be a boolean" };
69
+ patch.enabled = body.enabled;
70
+ }
71
+ if (body.model !== undefined) {
72
+ // An empty or whitespace-only value is the supported "clear the model" state, not an error:
73
+ // the resolver defaults to an empty model and `advisorRunnable` requires a non-blank one. The
74
+ // only structural rules are the string type and the trimmed length bound.
75
+ if (typeof body.model !== "string" || body.model.trim().length > 200) {
76
+ return {
77
+ ok: false,
78
+ code: "invalid_model",
79
+ message: "model must be a string whose trimmed value is at most 200 characters; an empty value clears the advisor model",
80
+ };
81
+ }
82
+ patch.model = body.model.trim();
83
+ }
84
+ if (body.effort !== undefined) {
85
+ if (!isValidAdvisorEffort(body.effort)) {
86
+ return { ok: false, code: "invalid_effort", message: `effort must be one of ${ADVISOR_EFFORTS.join(", ")}` };
87
+ }
88
+ patch.effort = body.effort;
89
+ }
90
+ if (body.policy !== undefined) {
91
+ if (!isValidAdvisorPolicy(body.policy)) {
92
+ return { ok: false, code: "invalid_policy", message: 'policy must be "manual", "preflight" or "adaptive"' };
93
+ }
94
+ patch.policy = body.policy;
95
+ }
96
+ if (body.timeoutMs !== undefined) {
97
+ if (typeof body.timeoutMs !== "number" || !Number.isFinite(body.timeoutMs) || body.timeoutMs < 1_000 || body.timeoutMs > 600_000) {
98
+ return { ok: false, code: "invalid_timeout", message: "timeoutMs must be a number between 1000 and 600000" };
99
+ }
100
+ patch.timeoutMs = Math.floor(body.timeoutMs);
101
+ }
102
+ if (body.maxConsultationsPerTask !== undefined) {
103
+ if (!isValidAdvisorTaskLimit(body.maxConsultationsPerTask)) return { ok: false, code: "invalid_task_limit", message: "maxConsultationsPerTask must be an integer between 1 and 3" };
104
+ patch.maxConsultationsPerTask = body.maxConsultationsPerTask;
105
+ }
106
+ if (body.contextSharingConsent !== undefined) {
107
+ if (body.contextSharingConsent === null) {
108
+ patch.contextSharingConsent = null;
109
+ } else if (isCurrentAdvisorContextSharingConsent(body.contextSharingConsent)) {
110
+ patch.contextSharingConsent = body.contextSharingConsent;
111
+ } else {
112
+ return {
113
+ ok: false,
114
+ code: "invalid_context_sharing_consent",
115
+ message: `contextSharingConsent must be "${ADVISOR_CONTEXT_SHARING_CONSENT_VERSION}" or null`,
116
+ };
117
+ }
118
+ }
119
+ return { ok: true, patch };
120
+ }
121
+
122
+ function advisorInfo(config: ManagementContext["config"]): Record<string, unknown> {
123
+ const settings = resolveAdvisorSettings(config);
124
+ const warning = settings.enabled && settings.model.trim() === ""
125
+ ? "advisor_enabled_without_model"
126
+ : advisorContextSharingBlocked(settings)
127
+ ? "advisor_context_sharing_consent_required"
128
+ : undefined;
129
+ return {
130
+ settings,
131
+ runnable: advisorRunnable(settings),
132
+ // Enabled without a model cannot consult. Enabled with a model but without current
133
+ // consent must not send task context; the warning names that block.
134
+ ...(warning ? { warning } : {}),
135
+ };
136
+ }
137
+
138
+ function applyPatchInMemory(config: ManagementContext["config"], patch: AdvisorPatch): void {
139
+ if (patch.reset) {
140
+ delete config.advisor;
141
+ return;
142
+ }
143
+ const current: Rec = isRec(config.advisor) ? { ...config.advisor } : {};
144
+ if (patch.enabled !== undefined) current.enabled = patch.enabled;
145
+ if (patch.model !== undefined) current.model = patch.model;
146
+ if (patch.effort !== undefined) current.effort = patch.effort;
147
+ if (patch.policy !== undefined) current.policy = patch.policy;
148
+ if (patch.timeoutMs !== undefined) current.timeoutMs = patch.timeoutMs;
149
+ if (patch.maxConsultationsPerTask !== undefined) current.maxConsultationsPerTask = patch.maxConsultationsPerTask;
150
+ if (patch.contextSharingConsent === null) delete current.contextSharingConsent;
151
+ else if (patch.contextSharingConsent !== undefined) current.contextSharingConsent = patch.contextSharingConsent;
152
+ config.advisor = current as ManagementContext["config"]["advisor"];
153
+ }
154
+
155
+ function isConfigLockContention(error: unknown): boolean {
156
+ if (!error || typeof error !== "object") return false;
157
+ if ((error as { code?: unknown }).code !== "CONFIG_MUTATION_LOCK_UNAVAILABLE") return false;
158
+ return (error as { cause?: { code?: unknown } }).cause?.code === "SQLITE_BUSY";
159
+ }
160
+
161
+ export async function handleAdvisorRoutes(ctx: ManagementContext): Promise<Response | null> {
162
+ const { url, req, config } = ctx;
163
+ if (url.pathname === "/api/advisor/activity" && req.method === "GET") {
164
+ return jsonResponse({ entries: advisorActivityStore(config).snapshot(), retention: "process", limit: 100 }, 200, req, config);
165
+ }
166
+ if (url.pathname !== "/api/advisor/settings") return null;
167
+
168
+ if (req.method === "GET") return jsonResponse(advisorInfo(config), 200, req, config);
169
+ if (req.method === "PUT") return putAdvisorSettings(ctx);
170
+ return null;
171
+ }
172
+
173
+ async function putAdvisorSettings(ctx: ManagementContext): Promise<Response> {
174
+ const { req, config } = ctx;
175
+
176
+ const body = await readManagementJsonBodyOr(req, INVALID_BODY);
177
+ const parsed = body === INVALID_BODY
178
+ ? { ok: false as const, code: "invalid_json", message: "body must be valid JSON" }
179
+ : parseAdvisorSettingsPatch(body);
180
+ if (!parsed.ok) return jsonResponse({ error: { code: parsed.code, message: parsed.message } }, 400, req, config);
181
+
182
+ // Resolve the writer BEFORE touching the live config: a resolution failure must leave the
183
+ // in-memory config exactly as it was, not rely on the snapshot restore below.
184
+ // `deps.` first: route tests with an in-memory fixture must never write the real config.
185
+ const persist = ctx.deps.saveConfigPreservingClaudeCode
186
+ ?? (await import("../../config")).saveConfigPreservingClaudeCode;
187
+ const snapshot = isRec(config.advisor) ? { ...config.advisor } : undefined;
188
+ applyPatchInMemory(config, parsed.patch);
189
+ try {
190
+ persist(config);
191
+ } catch (error) {
192
+ // Undo in memory too: a live config that serves a state the file does not hold would
193
+ // mislead every GET until the next restart.
194
+ if (snapshot === undefined) delete config.advisor;
195
+ else config.advisor = snapshot as ManagementContext["config"]["advisor"];
196
+ return isConfigLockContention(error)
197
+ ? jsonResponse({ error: { code: "config_busy", message: "Another process is saving the configuration. Try again in a moment." } }, 409, req, config)
198
+ : jsonResponse({ error: { code: "write_failed", message: "The configuration could not be saved." } }, 500, req, config);
199
+ }
200
+ activateAdvisor(config);
201
+ return jsonResponse(advisorInfo(config), 200, req, config);
202
+ }
@@ -1397,6 +1397,24 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1397
1397
  }
1398
1398
 
1399
1399
  // Claude Code inbound settings (GUI "Claude ON" toggle + Claude page).
1400
+ if (url.pathname === "/api/claude-code/model-defaults" && (req.method === "GET" || req.method === "POST")) {
1401
+ const { previewClaudeModelDefaults, applyClaudeModelDefaults, ModelDefaultsRecoveryError } = await import("../../claude/model-default-settings");
1402
+ try {
1403
+ if (req.method === "GET") return jsonResponse(previewClaudeModelDefaults(config.claudeCode?.tierModels));
1404
+ let body: unknown;
1405
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
1406
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length !== 1 ||
1407
+ !("fingerprint" in body) || typeof body.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(body.fingerprint)) {
1408
+ return jsonResponse({ error: "fingerprint is required" }, 400);
1409
+ }
1410
+ return jsonResponse(applyClaudeModelDefaults(config.claudeCode?.tierModels, body.fingerprint));
1411
+ } catch (error) {
1412
+ rethrowManagementBodyTooLarge(error);
1413
+ if (error instanceof ModelDefaultsRecoveryError) return jsonResponse({ error: error.message, code: error.message, backupPath: error.backupPath }, 500);
1414
+ const code = error instanceof Error && error.message === "settings_changed" ? "settings_changed" : "settings_unavailable";
1415
+ return jsonResponse({ error: code, code }, code === "settings_changed" ? 409 : 500);
1416
+ }
1417
+ }
1400
1418
  if (url.pathname === "/api/claude-code" && req.method === "GET") {
1401
1419
  const models = await fetchAllModels(config);
1402
1420
  const { listCatalogNativeSlugs } = await import("../../codex/catalog");
@@ -1513,10 +1531,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1513
1531
  });
1514
1532
  }
1515
1533
  if (url.pathname === "/api/claude-code" && req.method === "PUT") {
1516
- // NOTE: model / tierModels / maxContextTokens / alwaysEnableEffort are
1534
+ // NOTE: model / maxContextTokens / alwaysEnableEffort are
1517
1535
  // CONFIG-ONLY back-compat fields — the GUI no longer offers controls for them
1518
- // (default model is owned by Claude Code's /model picker; roster agents
1519
- // supersede tiers; auto-context supersedes the max-context pair; effort rides
1536
+ // (default model is owned by Claude Code's /model picker;
1537
+ // auto-context supersedes the max-context pair; effort rides
1520
1538
  // regardless on 2.1.207). PUT keeps validating them so hand-written configs
1521
1539
  // and older GUIs stay safe; GUI saves omit them and the spread preserves them.
1522
1540
  let parsedBody: unknown;
@@ -1835,7 +1853,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1835
1853
  }
1836
1854
  }
1837
1855
  if (body.tierModels !== undefined) {
1838
- // CONFIG-ONLY back-compat (GUI pickers removed — roster agents supersede tiers).
1856
+ // Family defaults also serve third-party callers that request native tier aliases.
1839
1857
  if (body.tierModels === null) {
1840
1858
  delete next.tierModels;
1841
1859
  } else if (!isPlainObject(body.tierModels)) {
@@ -44,7 +44,7 @@ export function pickerPreferenceOn(config: Pick<OcxConfig, "claudeCode">): boole
44
44
  }
45
45
 
46
46
  const PICKER_BODY_KEYS = new Set(["enabled", "persist", "trustedLocally", "callerAddedTrust"]);
47
- /** Enable outcomes that are progress, not refusal: on, waiting for a Desktop restart, or for the keychain step. */
47
+ /** Enable outcomes that are progress, not refusal: on, waiting for a Desktop restart, or for certificate trust. */
48
48
  const ENABLE_ACCEPTED = new Set(["active", "restart_required", "trust_pending"]);
49
49
 
50
50
  interface PickerRequestBody { enabled: boolean; persist: boolean; trustedLocally?: boolean; callerAddedTrust?: boolean }
@@ -52,6 +52,15 @@ import {
52
52
  setDebugSettings,
53
53
  type DebugFlag,
54
54
  } from "../../lib/debug-settings";
55
+ import { mergeModelCapabilities, modelCapabilitiesConfigError } from "../../config/provider-validation";
56
+ import { modelRecordValue } from "../../reasoning-effort";
57
+ import { commitProviderPatch } from "./provider-patch-transaction";
58
+ import { ConfigWritePublishedError } from "../../config/persist-unlocked";
59
+ import {
60
+ customRowInputModalities,
61
+ isVisionSidecarConsumer,
62
+ modelAcceptsImageInput,
63
+ } from "../../vision/eligibility";
55
64
  import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig, OcxComboCooldownWaitPolicy } from "../../types";
56
65
  import { drainAndShutdown } from "../lifecycle";
57
66
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
@@ -116,6 +125,43 @@ function sparseComboConfig<T extends {
116
125
  };
117
126
  }
118
127
 
128
+ interface VisionSidecarTarget {
129
+ provider: string;
130
+ model: string;
131
+ }
132
+
133
+ /**
134
+ * Request-only PUT field: combo members to declare text-only so the Vision Sidecar
135
+ * covers them when the combo accepts images. Every entry must be an exact target of
136
+ * the submitted combo, and the whole field is validated before any config mutation.
137
+ * Nothing from here is persisted under config.combos.
138
+ */
139
+ function parseVisionSidecarTargets(
140
+ raw: unknown,
141
+ comboTargetKeys: ReadonlySet<string>,
142
+ ): { targets?: VisionSidecarTarget[]; error?: string } {
143
+ if (raw === undefined) return {};
144
+ if (!Array.isArray(raw)) return { error: "visionSidecarTargets must be an array" };
145
+ const seen = new Set<string>();
146
+ const targets: VisionSidecarTarget[] = [];
147
+ for (const entry of raw) {
148
+ if (!isPlainRecord(entry)) return { error: "visionSidecarTargets entries must be objects" };
149
+ const provider = typeof entry.provider === "string" ? entry.provider.trim() : "";
150
+ const model = typeof entry.model === "string" ? entry.model.trim() : "";
151
+ if (!provider || !model) {
152
+ return { error: "visionSidecarTargets entries must have nonblank provider and model" };
153
+ }
154
+ const key = `${provider}/${model}`;
155
+ if (!comboTargetKeys.has(key)) {
156
+ return { error: `visionSidecarTargets entry "${key}" is not a target of this combo` };
157
+ }
158
+ if (seen.has(key)) continue;
159
+ seen.add(key);
160
+ targets.push({ provider, model });
161
+ }
162
+ return { targets };
163
+ }
164
+
119
165
  export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
120
166
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
121
167
 
@@ -176,6 +222,9 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
176
222
  const requestedCombo: Record<string, unknown> = body.combo;
177
223
  const effectiveCombo = {
178
224
  ...requestedCombo,
225
+ ...(!Object.hasOwn(requestedCombo, "allowMeteredTargets") && previous?.allowMeteredTargets === true
226
+ ? { allowMeteredTargets: true }
227
+ : {}),
179
228
  ...(!Object.hasOwn(requestedCombo, "cooldownMs") && previous?.cooldownMs !== undefined
180
229
  ? { cooldownMs: previous.cooldownMs }
181
230
  : {}),
@@ -203,7 +252,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
203
252
  // A non-record entry stays as the client sent it so comboConfigError reports it.
204
253
  // Reading `lastResort` off it here would throw in place of that structured 400.
205
254
  if (!isPlainRecord(target)) return target;
206
- if (Object.hasOwn(target, "lastResort")) return target;
255
+ if (["lastResort", "metered"].every(key => Object.hasOwn(target, key))) return target;
207
256
  const rawProvider = target.provider;
208
257
  const rawModel = target.model;
209
258
  if (typeof rawProvider !== "string" || typeof rawModel !== "string") return target;
@@ -214,7 +263,13 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
214
263
  const before = previous.targets.find(
215
264
  candidate => candidate.provider === provider && candidate.model === model,
216
265
  );
217
- return before?.lastResort ? { ...target, lastResort: true } : target;
266
+ // `metered` is carried the same way: dropping it on a GUI round-trip would open a
267
+ // pay-per-use target to every issued key.
268
+ return {
269
+ ...target,
270
+ ...(!Object.hasOwn(target, "lastResort") && before?.lastResort ? { lastResort: true } : {}),
271
+ ...(!Object.hasOwn(target, "metered") && before?.metered ? { metered: true } : {}),
272
+ };
218
273
  }),
219
274
  }
220
275
  : {}),
@@ -280,6 +335,54 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
280
335
  if (routeError) return jsonResponse({ error: issue ? `combo "${otherId}": ${routeError}` : routeError }, 400);
281
336
  }
282
337
  const normalized = normalizeComboConfig(effectiveCombo as unknown as OcxComboConfig);
338
+ const sidecarParsed = parseVisionSidecarTargets(
339
+ body.visionSidecarTargets,
340
+ new Set(normalized.targets.map((target) => `${target.provider}/${target.model}`)),
341
+ );
342
+ if (sidecarParsed.error) return jsonResponse({ error: sidecarParsed.error }, 400);
343
+ const sidecarPatches = new Map<string, Record<string, { inputModalities: string[] }>>();
344
+ if (sidecarParsed.targets?.length) {
345
+ if (normalized.imageInput === "disabled") {
346
+ return jsonResponse({ error: "visionSidecarTargets requires imageInput not disabled" }, 400);
347
+ }
348
+ for (const { provider, model } of sidecarParsed.targets) {
349
+ const providerRow = config.providers?.[provider];
350
+ if (!providerRow) {
351
+ return jsonResponse({ error: `visionSidecarTargets entry "${provider}/${model}" has no configured provider` }, 400);
352
+ }
353
+ // A text-only declaration would HIDE a real capability; never overwrite one.
354
+ if (modelAcceptsImageInput(config, { provider, id: model }) === true) {
355
+ return jsonResponse({ error: `visionSidecarTargets entry "${provider}/${model}" already accepts image input` }, 400);
356
+ }
357
+ // Already a consumer through any runtime source (exact declaration, custom row,
358
+ // noVisionModels, legacy record, registry enrichment): the catalog already
359
+ // advertises image for it. Writing the declaration again would be a no-op write.
360
+ if (isVisionSidecarConsumer(config, provider, model)) continue;
361
+ // Read the declaration the way the runtime does: exact capability axis, then the
362
+ // operator's custom row, then the provider's per-model modality hint. A row without
363
+ // text (audio-only) cannot be widened by the sidecar, and writing ["text"] on the
364
+ // capability axis would silently override that declaration.
365
+ const capabilityRow = Object.hasOwn(providerRow.modelCapabilities ?? {}, model)
366
+ ? providerRow.modelCapabilities?.[model] : undefined;
367
+ const declared = capabilityRow?.inputModalities
368
+ ?? customRowInputModalities(config, provider, model)
369
+ ?? modelRecordValue(providerRow.modelInputModalities, model);
370
+ if (declared !== undefined && !declared.includes("text")) {
371
+ return jsonResponse({ error: `visionSidecarTargets entry "${provider}/${model}" is declared without text input; the Vision Sidecar cannot cover it` }, 400);
372
+ }
373
+ const patch = sidecarPatches.get(provider) ?? {};
374
+ patch[model] = { inputModalities: ["text"] };
375
+ sidecarPatches.set(provider, patch);
376
+ }
377
+ for (const [providerName, patch] of sidecarPatches) {
378
+ // Validate the map that will be persisted, not just the patch: a provider already at
379
+ // the modelCapabilities cap would otherwise save an oversized map that load-time
380
+ // sanitization truncates, silently dropping the new text-only declaration.
381
+ const merged = mergeModelCapabilities(config.providers?.[providerName]?.modelCapabilities, patch);
382
+ const error = modelCapabilitiesConfigError(patch) ?? (merged ? modelCapabilitiesConfigError(merged) : null);
383
+ if (error) return jsonResponse({ error: `visionSidecarTargets for provider "${providerName}": ${error}` }, 400);
384
+ }
385
+ }
283
386
  // Persist only non-default identity/capability fields so config stays sparse.
284
387
  // Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
285
388
  // helper the GET/PUT responses use, so the wire shape and the stored shape cannot drift.
@@ -295,9 +398,10 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
295
398
  // would add a noise key to every target of every combo, including ones that never use
296
399
  // the policy (#5736). Only the opt-in value is stored, matching how the combo-level
297
400
  // policy is handled in sparseComboConfig.
298
- targets: normalizedBase.targets.map(({ lastResort, modelProfile, ...target }) => ({
401
+ targets: normalizedBase.targets.map(({ lastResort, metered, modelProfile, ...target }) => ({
299
402
  ...target,
300
403
  ...(lastResort ? { lastResort: true } : {}),
404
+ ...(metered ? { metered: true } : {}),
301
405
  ...(modelProfile ? { modelProfile } : {}),
302
406
  })),
303
407
  ...(normalizedAlias ? { alias: normalizedAlias } : {}),
@@ -340,58 +444,80 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
340
444
  const targetError = shadowCallTargetError({ ...config, combos: nextCombos }, migratedShadowTarget);
341
445
  if (targetError) return jsonResponse({ error: targetError }, 400);
342
446
  }
343
- config.combos = nextCombos;
344
- if (migratedModels.size > 0) {
345
- const migrateReference = (model: string): string => migratedModels.get(model) ?? model;
346
- const migrateAgentReference = (model: string): string => {
347
- const migrated = migrateReference(model);
348
- if (migrated !== model) shouldSyncClaudeAgentDefs = true;
349
- return migrated;
350
- };
351
- if (config.subagentModels) {
352
- config.subagentModels = [...new Set(config.subagentModels.map(migrateAgentReference))];
353
- }
354
- if (config.injectionModel && migratedModels.has(config.injectionModel)) {
355
- config.injectionModel = migrateReference(config.injectionModel);
356
- }
357
- if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
358
- config.shadowCallIntercept = {
359
- ...config.shadowCallIntercept,
360
- model: migrateReference(config.shadowCallIntercept.model),
361
- };
362
- }
363
- if (config.claudeCode) {
364
- const claudeCode = { ...config.claudeCode };
365
- for (const field of ["model", "smallFastModel"] as const) {
366
- if (claudeCode[field]) claudeCode[field] = migrateAgentReference(claudeCode[field]);
367
- }
368
- if (claudeCode.tierModels) {
369
- claudeCode.tierModels = Object.fromEntries(
370
- Object.entries(claudeCode.tierModels).map(([tier, model]) => [tier, migrateAgentReference(model)]),
371
- );
447
+ // One atomic transaction: a failed save must restore the combo, the capability
448
+ // declarations, and every migrated reference together, never partially.
449
+ try {
450
+ commitProviderPatch(config, () => {
451
+ config.combos = nextCombos;
452
+ if (migratedModels.size > 0) {
453
+ const migrateReference = (model: string): string => migratedModels.get(model) ?? model;
454
+ const migrateAgentReference = (model: string): string => {
455
+ const migrated = migrateReference(model);
456
+ if (migrated !== model) shouldSyncClaudeAgentDefs = true;
457
+ return migrated;
458
+ };
459
+ if (config.subagentModels) {
460
+ config.subagentModels = [...new Set(config.subagentModels.map(migrateAgentReference))];
461
+ }
462
+ if (config.injectionModel && migratedModels.has(config.injectionModel)) {
463
+ config.injectionModel = migrateReference(config.injectionModel);
464
+ }
465
+ if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
466
+ config.shadowCallIntercept = {
467
+ ...config.shadowCallIntercept,
468
+ model: migrateReference(config.shadowCallIntercept.model),
469
+ };
470
+ }
471
+ if (config.claudeCode) {
472
+ const claudeCode = { ...config.claudeCode };
473
+ for (const field of ["model", "smallFastModel"] as const) {
474
+ if (claudeCode[field]) claudeCode[field] = migrateAgentReference(claudeCode[field]);
475
+ }
476
+ if (claudeCode.tierModels) {
477
+ claudeCode.tierModels = Object.fromEntries(
478
+ Object.entries(claudeCode.tierModels).map(([tier, model]) => [tier, migrateAgentReference(model)]),
479
+ );
480
+ }
481
+ if (claudeCode.modelMap) {
482
+ claudeCode.modelMap = Object.fromEntries(
483
+ Object.entries(claudeCode.modelMap).map(([source, model]) => [source, migrateAgentReference(model)]),
484
+ );
485
+ }
486
+ if (claudeCode.intercept?.modelMap) {
487
+ claudeCode.intercept = {
488
+ ...claudeCode.intercept,
489
+ modelMap: Object.fromEntries(
490
+ Object.entries(claudeCode.intercept.modelMap).map(([pickerId, route]) => [pickerId, migrateAgentReference(route)]),
491
+ ),
492
+ };
493
+ }
494
+ config.claudeCode = claudeCode;
495
+ }
372
496
  }
373
- if (claudeCode.modelMap) {
374
- claudeCode.modelMap = Object.fromEntries(
375
- Object.entries(claudeCode.modelMap).map(([source, model]) => [source, migrateAgentReference(model)]),
376
- );
497
+ if (oldDisabledSelectors.size > 0 && config.disabledModels) {
498
+ config.disabledModels = [...new Set(config.disabledModels.map(model => (
499
+ oldDisabledSelectors.has(model) ? newDisabledModel : model
500
+ )))];
377
501
  }
378
- if (claudeCode.intercept?.modelMap) {
379
- claudeCode.intercept = {
380
- ...claudeCode.intercept,
381
- modelMap: Object.fromEntries(
382
- Object.entries(claudeCode.intercept.modelMap).map(([pickerId, route]) => [pickerId, migrateAgentReference(route)]),
383
- ),
384
- };
502
+ for (const [providerName, patch] of sidecarPatches) {
503
+ const provider = config.providers?.[providerName];
504
+ if (!provider) continue;
505
+ const capabilities = mergeModelCapabilities(provider.modelCapabilities, patch);
506
+ // Re-check under the config lock: a concurrent edit may have grown the map since
507
+ // the pre-check, and an oversized map must never be published.
508
+ if (capabilities && modelCapabilitiesConfigError(capabilities)) {
509
+ throw new Error("modelCapabilities would exceed its limit");
510
+ }
511
+ if (capabilities === undefined) delete provider.modelCapabilities;
512
+ else provider.modelCapabilities = capabilities;
385
513
  }
386
- config.claudeCode = claudeCode;
514
+ }, deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode);
515
+ } catch (error) {
516
+ if (!(error instanceof ConfigWritePublishedError)) {
517
+ return jsonResponse({ error: "combo could not be saved" }, 500);
387
518
  }
519
+ throw error;
388
520
  }
389
- if (oldDisabledSelectors.size > 0 && config.disabledModels) {
390
- config.disabledModels = [...new Set(config.disabledModels.map(model => (
391
- oldDisabledSelectors.has(model) ? newDisabledModel : model
392
- )))];
393
- }
394
- saveConfigPreservingClaudeCode(config);
395
521
  reconcileLiveStateStores();
396
522
  clearComboSelectionState(id);
397
523
  clearComboTargetCooldowns(id);
@@ -27,6 +27,10 @@ function response(): Response {
27
27
  }
28
28
 
29
29
  export async function handleCompanionRoutes(ctx: ManagementContext): Promise<Response | null> {
30
+ if (ctx.url.pathname === "/api/advisor/settings" || ctx.url.pathname === "/api/advisor/activity") {
31
+ const { handleAdvisorRoutes } = await import("./advisor-routes");
32
+ return handleAdvisorRoutes(ctx);
33
+ }
30
34
  if (ctx.url.pathname === "/api/companion/open-in-browser" && ctx.req.method === "POST") {
31
35
  let body: unknown;
32
36
  try {