@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,14 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import type { CatalogModel } from "../../codex/catalog";
4
- import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
5
  import {
6
6
  DEFAULT_SUBAGENT_MODELS,
7
7
  codexAutoStartEnabled,
8
+ deleteConfigTopLevelKey,
8
9
  hasOwnProvider,
9
10
  isValidProviderName,
10
11
  loadConfig,
11
12
  multiAgentGuidanceEnabled,
13
+ mutatePersistedConfig,
12
14
  providerBaseUrlConfigError,
13
15
  providerHeadersConfigError,
14
16
  saveConfigPreservingClaudeCode,
@@ -30,7 +32,7 @@ import { deriveProviderPresets } from "../../providers/derive";
30
32
  import { providerCodexAccountMode } from "../../providers/registry";
31
33
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
32
34
  import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
33
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
35
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
34
36
  import { clearThreadAccountMap } from "../../codex/routing";
35
37
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
36
38
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
@@ -50,6 +52,17 @@ import {
50
52
  type DebugFlag,
51
53
  } from "../../lib/debug-settings";
52
54
  import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
55
+ import {
56
+ visionCandidateRows,
57
+ visionDescriberIsProvablyBlind,
58
+ visionDescriberRejection,
59
+ } from "./vision-sidecar-options";
60
+ import {
61
+ webSearchCandidateRows,
62
+ webSearchModelIsRejected,
63
+ webSearchModelRejection,
64
+ type WebSearchBackend,
65
+ } from "./web-search-sidecar-options";
53
66
  import { drainAndShutdown } from "../lifecycle";
54
67
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
55
68
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
@@ -59,7 +72,7 @@ import { applySystemEnvToggle } from "../system-env";
59
72
 
60
73
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
61
74
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
62
- import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
75
+ import { readManagementJsonBody, readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
63
76
 
64
77
  const GROK_APPLY_JOIN_MS = 120_000;
65
78
  export const GROK_APPLY_TERMINAL_MS = 10 * 60_000;
@@ -70,6 +83,49 @@ let grokApplyTestHooks: { now?: () => number; run?: () => Promise<unknown> } | n
70
83
 
71
84
  class GrokApplyBusyError extends Error {}
72
85
 
86
+ /**
87
+ * Mirror a durable desired-state transition onto the long-lived server snapshot.
88
+ *
89
+ * `setIntegrationEnabled` writes DISK only. The server reuses one `config` object
90
+ * for every request, so leaving it stale makes the native GET report the opposite
91
+ * of what was just persisted, and makes any later whole-snapshot save (the Desktop
92
+ * profile PUT does exactly that) write the stale value back over the transition.
93
+ * ON is the ABSENCE of the key, matching `setIntegrationEnabled`'s on-disk shape.
94
+ */
95
+ function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-desktop", enabled: boolean): void {
96
+ const integrations = { ...(config.clientIntegrations ?? {}) };
97
+ if (enabled) delete integrations[client];
98
+ else integrations[client] = false;
99
+ if (Object.keys(integrations).length === 0) deleteConfigTopLevelKey(config, "clientIntegrations");
100
+ else config.clientIntegrations = integrations;
101
+ }
102
+
103
+ /**
104
+ * Persist ONLY `claudeCode.desktopProfile`, field-scoped, against the CURRENT
105
+ * on-disk config.
106
+ *
107
+ * `saveConfigPreservingClaudeCode(ctx.config)` writes the whole long-lived server
108
+ * snapshot. On the apply path that snapshot still carries the `clientIntegrations`
109
+ * it was loaded with, so a save right after `setIntegrationEnabled("claude-desktop",
110
+ * true)` carried the stale OFF back over the enable and made the route cancel its
111
+ * own apply. Mutating one field under the config-mutation lock cannot regress an
112
+ * unrelated key another writer just committed.
113
+ */
114
+ function persistDesktopProfileField(
115
+ config: OcxConfig,
116
+ desktopProfile: NonNullable<OcxConfig["claudeCode"]>["desktopProfile"],
117
+ ): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" } {
118
+ const outcome = mutatePersistedConfig(persisted => {
119
+ persisted.claudeCode = { ...(persisted.claudeCode ?? {}), desktopProfile };
120
+ return { changed: true, value: true };
121
+ });
122
+ // Only mirror into memory once the durable write actually landed; an
123
+ // `unavailable` outcome must not leave the snapshot claiming a saved profile.
124
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
125
+ config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile };
126
+ return { ok: true };
127
+ }
128
+
73
129
  export function grokApplyFlightSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
74
130
  return {
75
131
  currentBytes: grokApplyFlight?.bytes ?? 0,
@@ -94,7 +150,7 @@ function runGrokApplyFlight(): Promise<unknown> {
94
150
  flight.promise = (grokApplyTestHooks?.run ?? (async () => {
95
151
  const [{ syncGrokConfig }, { readRuntimePort }] = await Promise.all([
96
152
  import("../../grok/sync"),
97
- import("../../config"),
153
+ import("../../config/process-state"),
98
154
  ]);
99
155
  const currentConfig = loadConfig();
100
156
  const runtime = readRuntimePort(process.pid);
@@ -125,28 +181,44 @@ export function setGrokApplyFlightTestHooks(
125
181
  import type { ManagementContext } from "./context";
126
182
 
127
183
  export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
128
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
184
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
129
185
 
130
186
  /** Best-effort Desktop 3P config auto-reconcile when providers change. */
131
187
  async function autoApplyDesktopBestEffort(): Promise<void> {
132
188
  try {
133
- if (config.claudeCode?.desktopAutoApply === false) return;
134
- if (!config.claudeCode?.desktopProfile) return;
135
- const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
189
+ const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
190
+ const admitted = loadConfig();
191
+ if (!claudeDesktopIntegrationEnabled(admitted)) return;
192
+ if (admitted.claudeCode?.desktopAutoApply === false) return;
193
+ if (!admitted.claudeCode?.desktopProfile) return;
194
+ const { inspectDesktop3pConfigLibrary, writeDesktop3pConfig } = await import("../../claude/desktop-3p");
195
+ const beforeKind = inspectDesktop3pConfigLibrary({
196
+ appliedFingerprint: admitted.claudeCode.desktopProfile.appliedFingerprint ?? null,
197
+ }).kind;
198
+ if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(beforeKind)) return;
136
199
  const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
137
- const allModels = await fetchAllModels(config);
138
- const routed = filterCatalogVisibleModels(allModels, config).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
139
- const result = writeDesktop3pConfig(
140
- config.port ?? 10100,
141
- [...desktopVisibleNativeSlugs(config)],
200
+ const allModels = await (deps.fetchAllModels ?? fetchAllModels)(admitted);
201
+ const current = loadConfig();
202
+ // This is the real guard: the catalog await admits a concurrent explicit OFF.
203
+ if (!claudeDesktopIntegrationEnabled(current)) return;
204
+ if (current.claudeCode?.desktopAutoApply === false || !current.claudeCode?.desktopProfile) return;
205
+ const afterKind = inspectDesktop3pConfigLibrary({
206
+ appliedFingerprint: current.claudeCode.desktopProfile.appliedFingerprint ?? null,
207
+ }).kind;
208
+ if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(afterKind)) return;
209
+ const routed = filterCatalogVisibleModels(allModels, current).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
210
+ const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
211
+ current.port ?? 10100,
212
+ [...desktopVisibleNativeSlugs(current)],
142
213
  routed,
143
- config.apiKeys?.[0]?.key,
214
+ current.apiKeys?.[0]?.key,
144
215
  "static",
145
- config.claudeCode.desktopProfile,
216
+ current.claudeCode.desktopProfile,
217
+ nativeContextLimits(current),
146
218
  );
147
219
  if (result.written && result.fingerprint) {
148
- config.claudeCode = { ...config.claudeCode, desktopProfile: { ...config.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
149
- saveConfigPreservingClaudeCode(config);
220
+ current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
221
+ saveConfigPreservingClaudeCode(current);
150
222
  }
151
223
  } catch { /* best-effort */ }
152
224
  }
@@ -160,6 +232,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
160
232
  const {
161
233
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads,
162
234
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
235
+ getMultiAgentModeHintText,
163
236
  } = await import("../../codex/features");
164
237
  const enabled = isMultiAgentV2Enabled();
165
238
  return jsonResponse({
@@ -167,9 +240,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
167
240
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
168
241
  maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
169
242
  multiAgentMode: config.multiAgentMode ?? "default",
243
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
170
244
  agentsEnabled: getAgentsEnabled(),
171
245
  agentsMaxDepth: getAgentsMaxDepth(),
172
246
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
247
+ multiAgentModeHintText: getMultiAgentModeHintText(),
173
248
  // max_depth is V1-only upstream; this is the global-flag statement, derived
174
249
  // server-side so no client can present it as an effective V2 limit.
175
250
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
@@ -180,24 +255,31 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
180
255
  enabled?: unknown;
181
256
  maxConcurrentThreadsPerSession?: unknown;
182
257
  multiAgentMode?: unknown;
258
+ keepNativeChatGptOnV1?: unknown;
183
259
  agentsEnabled?: unknown;
184
260
  agentsMaxDepth?: unknown;
185
261
  subagentDeveloperInstructions?: unknown;
262
+ multiAgentModeHintText?: unknown;
186
263
  };
187
264
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
188
265
  const wantsFlag = body.enabled !== undefined;
189
266
  const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
190
267
  const wantsMode = body.multiAgentMode !== undefined;
268
+ const wantsKeepNative = body.keepNativeChatGptOnV1 !== undefined;
191
269
  const wantsAgentsEnabled = body.agentsEnabled !== undefined;
192
270
  const wantsMaxDepth = body.agentsMaxDepth !== undefined;
193
271
  const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
194
- if (!wantsFlag && !wantsThreads && !wantsMode && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions) {
195
- return jsonResponse({ error: "body must set enabled, multiAgentMode, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, and/or subagentDeveloperInstructions" }, 400);
272
+ const wantsModeHintText = body.multiAgentModeHintText !== undefined;
273
+ if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
274
+ return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
196
275
  }
197
276
  if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
198
277
  if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
199
278
  return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
200
279
  }
280
+ if (wantsKeepNative && typeof body.keepNativeChatGptOnV1 !== "boolean") {
281
+ return jsonResponse({ error: "body.keepNativeChatGptOnV1 must be a boolean" }, 400);
282
+ }
201
283
  if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
202
284
  return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
203
285
  }
@@ -216,29 +298,58 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
216
298
  if (wantsSubagentInstructions && body.subagentDeveloperInstructions !== null && typeof body.subagentDeveloperInstructions !== "string") {
217
299
  return jsonResponse({ error: "body.subagentDeveloperInstructions must be a string or null" }, 400);
218
300
  }
301
+ // null unsets the upstream key (effort-derived policy resumes); an empty/whitespace
302
+ // string is rejected because codex-rs treats any present hint as an override that
303
+ // suppresses even the Ultra-derived Proactive message (Option<String>, no blank
304
+ // special-case in effective_multi_agent_mode).
305
+ if (wantsModeHintText && body.multiAgentModeHintText !== null
306
+ && (typeof body.multiAgentModeHintText !== "string" || body.multiAgentModeHintText.trim().length === 0)) {
307
+ return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
308
+ }
219
309
  const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
220
- const modeFlag = mode === "v2" ? true : mode === "v1" ? false : undefined;
310
+ const effectiveMode = mode ?? config.multiAgentMode ?? "default";
311
+ const effectiveKeepNative = wantsKeepNative
312
+ ? body.keepNativeChatGptOnV1 === true
313
+ : config.keepNativeChatGptOnV1 === true;
314
+ const hybridPinActive = effectiveMode === "v2" && effectiveKeepNative;
315
+ const modeFlag = mode === "v2" ? !hybridPinActive : mode === "v1" ? false : undefined;
221
316
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
222
- return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
317
+ return jsonResponse({
318
+ error: hybridPinActive
319
+ ? "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins"
320
+ : `body.enabled conflicts with multiAgentMode '${mode}'`,
321
+ }, 400);
223
322
  }
224
323
  const {
225
324
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
226
325
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
227
- setAgentsEnabled, setAgentsMaxDepth, setSubagentDeveloperInstructions,
326
+ getMultiAgentModeHintText, probeCodexSupportsModeHint, setAgentsEnabled, setAgentsMaxDepth,
327
+ setSubagentDeveloperInstructions, setMultiAgentModeHintText, MODE_HINT_UNSUPPORTED_ERROR,
228
328
  } = await import("../../codex/features");
329
+ // Probe the capability before any combined-request mutation. The scalar writer
330
+ // repeats this check, but doing it here prevents an earlier flag/mode/agents
331
+ // write from landing before an unsupported runtime returns 502.
332
+ if (wantsModeHintText && body.multiAgentModeHintText !== null
333
+ && probeCodexSupportsModeHint() === false) {
334
+ return jsonResponse({
335
+ error: `writing multiAgentModeHintText failed: ${MODE_HINT_UNSUPPORTED_ERROR}`,
336
+ }, 502);
337
+ }
229
338
  const warnings: string[] = [];
230
- const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
339
+ if (wantsFlag && body.enabled === true && hybridPinActive) {
340
+ return jsonResponse({
341
+ error: "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins",
342
+ }, 400);
343
+ }
344
+ const requestedFlag = wantsFlag
345
+ ? body.enabled as boolean
346
+ : modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
231
347
  if (requestedFlag !== undefined || wantsThreads) {
232
- const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
233
- let toggle = deps.toggleCodexMultiAgentV2;
234
- if (!toggle) {
235
- const { execFileSync } = await import("node:child_process");
236
- const { codexFeaturesInvocation } = await import("../../cli/v2");
237
- toggle = (enabled: boolean) => {
238
- const inv = codexFeaturesInvocation(enabled ? "enable" : "disable");
239
- execFileSync(inv.file, inv.args,
240
- { stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true, ...inv.options });
241
- };
348
+ const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
349
+ let toggle = deps.toggleCodexMultiAgentV2;
350
+ if (!toggle) {
351
+ const { runCodexFeaturesCommand } = await import("../../cli/v2");
352
+ toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable");
242
353
  }
243
354
  const result = transitionMultiAgentV2(targetFlag, toggle, {
244
355
  ...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
@@ -247,11 +358,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
247
358
  if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
248
359
  }
249
360
  if (wantsMode) {
250
- if (mode === "default") delete config.multiAgentMode;
361
+ if (mode === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
251
362
  else config.multiAgentMode = mode;
252
363
  saveConfigPreservingClaudeCode(config);
253
364
  warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
254
365
  }
366
+ if (wantsKeepNative) {
367
+ if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
368
+ else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
369
+ saveConfigPreservingClaudeCode(config);
370
+ warnings.push(body.keepNativeChatGptOnV1 === true
371
+ ? (effectiveMode === "v2"
372
+ ? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
373
+ : "keepNativeChatGptOnV1 is stored but inactive until multi-agent mode is v2. Applies to new sessions.")
374
+ : "ChatGPT-native models follow the selected v1/v2/base surface. Applies to new sessions.");
375
+ }
255
376
  // New-key scalar writes: each writer is individually atomic, so apply them in
256
377
  // sequence after the transition. A failure here is a persistence failure (the
257
378
  // writers' ok:false result or a throw from the underlying atomic write helper),
@@ -263,6 +384,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
263
384
  if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
264
385
  if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
265
386
  if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
387
+ if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
266
388
  const landed: string[] = [];
267
389
  for (const write of scalarWrites) {
268
390
  try {
@@ -282,7 +404,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
282
404
  if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
283
405
  warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
284
406
  }
285
- await refreshCodexCatalogBestEffort();
407
+ const catalogRefresh = await convergeCodexCatalog();
286
408
  if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
287
409
  const enabled = isMultiAgentV2Enabled();
288
410
  return jsonResponse({
@@ -291,14 +413,71 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
291
413
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
292
414
  maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
293
415
  multiAgentMode: config.multiAgentMode ?? "default",
416
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
294
417
  agentsEnabled: getAgentsEnabled(),
295
418
  agentsMaxDepth: getAgentsMaxDepth(),
296
419
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
420
+ multiAgentModeHintText: getMultiAgentModeHintText(),
297
421
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
298
422
  warnings,
423
+ catalogRefresh,
299
424
  });
300
425
  }
301
426
 
427
+ // default_mode_request_user_input feature toggle (Codex Auth page). GET reads the
428
+ // flag from $CODEX_HOME/config.toml; PUT flips it via the official `codex features`
429
+ // CLI so the TOML edit stays upstream-owned and format-preserving.
430
+ if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "GET") {
431
+ const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
432
+ return jsonResponse({
433
+ enabled: isDefaultModeRequestUserInputEnabled(),
434
+ key: DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY,
435
+ });
436
+ }
437
+ if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "PUT") {
438
+ let parsedBody: unknown;
439
+ try {
440
+ parsedBody = await readManagementJsonBody(req);
441
+ } catch (error) {
442
+ rethrowManagementBodyTooLarge(error);
443
+ return jsonResponse({ error: "invalid JSON body" }, 400);
444
+ }
445
+ if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
446
+ return jsonResponse({ error: "body must be a JSON object" }, 400);
447
+ }
448
+ const body = parsedBody as { enabled?: unknown };
449
+ if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
450
+ const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
451
+ const before = isDefaultModeRequestUserInputEnabled();
452
+ let toggle = deps.toggleDefaultModeRequestUserInput;
453
+ if (!toggle) {
454
+ const { runCodexFeaturesCommand } = await import("../../cli/v2");
455
+ toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable", DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY);
456
+ }
457
+ let toggleError: string | null = null;
458
+ try {
459
+ toggle(body.enabled);
460
+ } catch (error) {
461
+ const err = error as { stderr?: unknown; message?: string };
462
+ const raw = err.stderr;
463
+ const stderrText = typeof raw === "string"
464
+ ? raw.trim()
465
+ : raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
466
+ toggleError = stderrText || (err.message ?? String(error));
467
+ }
468
+ const enabled = isDefaultModeRequestUserInputEnabled();
469
+ if (toggleError !== null || enabled !== body.enabled) {
470
+ const reason = toggleError
471
+ ?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
472
+ return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
473
+ }
474
+ const warnings: string[] = [];
475
+ if (enabled !== before) {
476
+ warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
477
+ }
478
+ return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
479
+ }
480
+
302
481
  // Subagent prompt injection model: single native or routed model whose info is
303
482
  // dynamically injected into the v1 proactive prompt, plus an optional reasoning
304
483
  // effort the prompt tells the agent to pass to spawn_agent. GET returns the current
@@ -396,13 +575,13 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
396
575
 
397
576
  config.multiAgentGuidanceEnabled = nextEnabled;
398
577
  if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
399
- else delete config.syncCodexSubagentDefaults;
578
+ else deleteConfigTopLevelKey(config, "syncCodexSubagentDefaults");
400
579
  if (nextModel) config.injectionModel = nextModel;
401
- else delete config.injectionModel;
580
+ else deleteConfigTopLevelKey(config, "injectionModel");
402
581
  if (nextEffort) config.injectionEffort = nextEffort;
403
- else delete config.injectionEffort;
582
+ else deleteConfigTopLevelKey(config, "injectionEffort");
404
583
  if (nextPrompt) config.injectionPrompt = nextPrompt;
405
- else delete config.injectionPrompt;
584
+ else deleteConfigTopLevelKey(config, "injectionPrompt");
406
585
 
407
586
  saveConfigPreservingClaudeCode(config);
408
587
  return jsonResponse({
@@ -433,7 +612,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
433
612
  for (const key of ["effortCap", "subagentEffortCap"] as const) {
434
613
  if (!(key in body)) continue;
435
614
  const value = body[key];
436
- if (value === null || value === "") { delete config[key]; continue; }
615
+ if (value === null || value === "") { deleteConfigTopLevelKey(config, key); continue; }
437
616
  if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
438
617
  return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
439
618
  }
@@ -456,15 +635,29 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
456
635
  stored === catalogModelSlug(m) || slugEquals(stored, m.provider, m.id)
457
636
  ))
458
637
  .map(catalogModelSlug))];
459
- const available = [
638
+ const chosen = config.subagentModels ?? [];
639
+ const selectable = [
460
640
  ...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
461
641
  ...visibleRouted,
462
642
  ];
643
+ // A saved roster slot must stay representable even after its model is disabled
644
+ // elsewhere (Models page, provider allowlist, a provider row going away). The
645
+ // dashboard treats `available` as the set of rows it can render, so a chosen id
646
+ // missing from it disappears from the roster UI and the next Save — which PUTs
647
+ // exactly what the UI holds — silently truncates the persisted list. Losing a
648
+ // deliberate 5-model roster to an unrelated visibility toggle is data loss, not a
649
+ // filter. Same reasoning as `fetchGrokCandidateModels`, which deliberately lists a
650
+ // model the user already excluded so its switch remains reachable.
651
+ const selectableSet = new Set(selectable);
652
+ const available = [
653
+ ...selectable,
654
+ ...[...new Set(chosen)].filter(model => !selectableSet.has(model)),
655
+ ];
463
656
  // #857: let CLI/GUI show when a running Codex app-server keeps an older
464
657
  // in-memory catalog than the one on disk.
465
658
  const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
466
659
  const catalogState = collectCodexAppServerCatalogState();
467
- return jsonResponse({ chosen: config.subagentModels ?? [], available, catalogState });
660
+ return jsonResponse({ chosen, available, catalogState });
468
661
  }
469
662
  if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
470
663
  let body: { models?: unknown };
@@ -473,10 +666,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
473
666
  config.subagentModels = chosen;
474
667
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
475
668
  save(config);
476
- await refreshCodexCatalogBestEffort();
669
+ const catalogRefresh = await convergeCodexCatalog();
477
670
  await syncClaudeAgentDefsBestEffort();
478
671
  await autoApplyDesktopBestEffort();
479
- return jsonResponse({ ok: true, applied: chosen });
672
+ return jsonResponse({ ok: true, applied: chosen, catalogRefresh });
480
673
  }
481
674
 
482
675
  // Priority-ordered subagent model fallback chain for quota-aware spawn routing.
@@ -538,9 +731,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
538
731
  }
539
732
  }
540
733
  if (nextModels !== undefined) config.subagentModelFallback = nextModels;
541
- else delete config.subagentModelFallback;
734
+ else deleteConfigTopLevelKey(config, "subagentModelFallback");
542
735
  if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
543
- else delete config.subagentModelFallbackPollMs;
736
+ else deleteConfigTopLevelKey(config, "subagentModelFallbackPollMs");
544
737
  saveConfigPreservingClaudeCode(config);
545
738
  return jsonResponse({
546
739
  ok: true,
@@ -581,7 +774,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
581
774
  // cannot grow config.json without bound.
582
775
  const excluded = [...new Set(raw as string[])].sort();
583
776
  if (excluded.length > 2000) return jsonResponse({ error: "excluded list is too large" }, 400);
584
- if (excluded.length === 0) delete config.grokExcludedModels;
777
+ if (excluded.length === 0) deleteConfigTopLevelKey(config, "grokExcludedModels");
585
778
  else config.grokExcludedModels = excluded;
586
779
  saveConfigPreservingClaudeCode(config);
587
780
  return jsonResponse({ ok: true, excluded });
@@ -783,21 +976,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
783
976
  // #859: the CLI delegates here so the registry is built in the serving
784
977
  // process. Accept an optional mode; default stays static for back-compat.
785
978
  let mode: "static" | "hybrid" | "discovery" = "static";
786
- const rawBody = await req.text();
787
979
  let parsed: unknown;
788
- if (rawBody.trim()) {
789
- try {
790
- parsed = JSON.parse(rawBody);
791
- } catch {
792
- return jsonResponse({ error: "invalid JSON body" }, 400);
793
- }
794
- const requested = (parsed as { mode?: unknown } | null)?.mode;
795
- if (requested !== undefined) {
796
- if (requested === "static" || requested === "hybrid" || requested === "discovery") {
797
- mode = requested;
798
- } else {
799
- return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
800
- }
980
+ try {
981
+ parsed = await readOptionalManagementJsonBody(req);
982
+ } catch (error) {
983
+ rethrowManagementBodyTooLarge(error);
984
+ return jsonResponse({ error: "invalid JSON body" }, 400);
985
+ }
986
+ const requested = (parsed as { mode?: unknown } | null)?.mode;
987
+ if (requested !== undefined) {
988
+ if (requested === "static" || requested === "hybrid" || requested === "discovery") {
989
+ mode = requested;
990
+ } else {
991
+ return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
801
992
  }
802
993
  }
803
994
  // #859: a delegated CLI apply carries the profile it just saved — the
@@ -813,9 +1004,26 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
813
1004
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
814
1005
  }
815
1006
  }
1007
+ const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
1008
+ const desired = setIntegrationEnabled("claude-desktop", true);
1009
+ if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
1010
+ // Disk now says ON; the reused server snapshot must agree, or the native
1011
+ // GET reports OFF and a later whole-snapshot save undoes this transition.
1012
+ mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
816
1013
  const state = await buildClaudeDesktopState(config, profileOverride);
817
- config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
818
- saveConfigPreservingClaudeCode(config);
1014
+ // `setIntegrationEnabled` above wrote desired ON to DISK; it does not touch
1015
+ // this long-lived server snapshot. Saving the snapshot wholesale would carry
1016
+ // its stale `clientIntegrations` back over that write and turn the enable
1017
+ // action into an immediate self-cancelling OFF — the guard below would then
1018
+ // refuse the apply it was asked to perform. Persist ONLY the profile field.
1019
+ const profileSaved = persistDesktopProfileField(config, state.profile);
1020
+ if (!profileSaved.ok) {
1021
+ return jsonResponse({
1022
+ error: `Claude Desktop profile could not be saved (${profileSaved.reason}); nothing was applied.`,
1023
+ saved: false,
1024
+ applied: false,
1025
+ }, profileSaved.reason === "conflict" ? 409 : 500);
1026
+ }
819
1027
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
820
1028
  const { desktopVisibleNativeSlugs } = await import("../../codex/catalog");
821
1029
  const routed = state.models
@@ -824,22 +1032,52 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
824
1032
  const slash = model.route.indexOf("/");
825
1033
  return { provider: model.route.slice(0, slash), id: model.route.slice(slash + 1), contextWindow: model.contextWindow };
826
1034
  });
827
- const result = writeDesktop3pConfig(
828
- Number(url.port) || config.port,
829
- [...desktopVisibleNativeSlugs(config)],
1035
+ // State construction can await catalog work; never write from the stale
1036
+ // config captured before that await if another request turned Desktop off.
1037
+ const latest = loadConfig();
1038
+ if (!claudeDesktopIntegrationEnabled(latest)) {
1039
+ return jsonResponse({
1040
+ error: "Claude Desktop apply was cancelled because the desired state changed to off.",
1041
+ code: "claude_desktop_apply_skipped",
1042
+ reason: "desired_state_changed",
1043
+ desiredEnabled: false,
1044
+ saved: true,
1045
+ applied: false,
1046
+ }, 409);
1047
+ }
1048
+ const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
1049
+ Number(url.port) || latest.port,
1050
+ [...desktopVisibleNativeSlugs(latest)],
830
1051
  routed,
831
- config.apiKeys?.[0]?.key,
1052
+ latest.apiKeys?.[0]?.key,
832
1053
  mode,
833
1054
  state.profile,
1055
+ nativeContextLimits(latest),
834
1056
  );
835
1057
  if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
836
1058
  // Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
837
1059
  if (result.fingerprint) {
838
- config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: { ...state.profile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
839
- saveConfigPreservingClaudeCode(config);
1060
+ // The Desktop write already landed, so a failed bookkeeping save is not
1061
+ // an apply failure: report the miss instead of claiming a clean apply.
1062
+ const marked = persistDesktopProfileField(config, {
1063
+ ...state.profile,
1064
+ appliedFingerprint: result.fingerprint,
1065
+ appliedAt: new Date().toISOString(),
1066
+ });
1067
+ if (!marked.ok) {
1068
+ return jsonResponse({
1069
+ ok: true,
1070
+ applied: true,
1071
+ saved: false,
1072
+ path: result.path,
1073
+ fingerprint: result.fingerprint,
1074
+ warning: `Claude Desktop was applied, but the applied marker was not saved (${marked.reason}).`,
1075
+ });
1076
+ }
840
1077
  }
841
1078
  return jsonResponse({ ok: true, saved: true, applied: true, path: result.path, fingerprint: result.fingerprint });
842
1079
  } catch (error) {
1080
+ rethrowManagementBodyTooLarge(error);
843
1081
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
844
1082
  }
845
1083
  }
@@ -847,47 +1085,35 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
847
1085
  // Desktop applied-state + health status.
848
1086
  if (url.pathname === "/api/claude-desktop/status" && req.method === "GET") {
849
1087
  try {
850
- const { readFileSync: readFile, existsSync } = await import("node:fs");
851
- const { createHash } = await import("node:crypto");
852
- const { join } = await import("node:path");
853
- const { resolveDesktop3pConfigLibraryPath } = await import("../../claude/desktop-3p");
854
- const libraryPath = resolveDesktop3pConfigLibraryPath();
855
- const metaPath = join(libraryPath, "_meta.json");
856
- let onDiskFingerprint: string | null = null;
857
- let configPath: string | null = null;
858
- // Desktop serves ONLY the profile named by _meta.json's appliedId, so an
859
- // opencodex entry that merely EXISTS does not mean Desktop is using it.
860
- // null = undeterminable (no metadata / unreadable / no appliedId).
861
- let activeProfile: boolean | null = null;
862
- if (existsSync(metaPath)) {
863
- try {
864
- const meta = JSON.parse(readFile(metaPath, "utf8"));
865
- const entry = Array.isArray(meta.entries) ? meta.entries.find((e: { name?: string }) => e?.name === "opencodex") : undefined;
866
- const appliedId = typeof meta.appliedId === "string" ? meta.appliedId : null;
867
- // A readable appliedId with no opencodex entry is a KNOWN false, not unknown.
868
- activeProfile = appliedId === null ? null : (entry?.id ? appliedId === entry.id : false);
869
- if (entry?.id) {
870
- configPath = join(libraryPath, `${entry.id}.json`);
871
- if (existsSync(configPath)) {
872
- const onDisk = readFile(configPath, "utf8");
873
- onDiskFingerprint = createHash("sha256").update(onDisk).digest("hex").slice(0, 16);
874
- }
875
- }
876
- } catch { /* unreadable metadata */ }
877
- }
878
- const savedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
879
- const appliedAt = config.claudeCode?.desktopProfile?.appliedAt ?? null;
880
- const stale = savedFingerprint !== null && onDiskFingerprint !== null && savedFingerprint !== onDiskFingerprint;
1088
+ const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
1089
+ const { inspectDesktop3pConfigLibrary } = await import("../../claude/desktop-3p");
1090
+ const persisted = loadConfig();
1091
+ const savedFingerprint = persisted.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
1092
+ const observed = inspectDesktop3pConfigLibrary({ appliedFingerprint: savedFingerprint });
1093
+ const desiredEnabled = claudeDesktopIntegrationEnabled(persisted);
1094
+ const applied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
1095
+ // "Needs update" is only meaningful while the integration is wanted. When the
1096
+ // durable switch is OFF, a leftover drifted profile is residue to clear not
1097
+ // a stale apply the operator should refresh.
1098
+ const stale = desiredEnabled && observed.kind === "gateway_drifted";
881
1099
  const { getDesktopHealth } = await import("../../claude/desktop-health");
882
1100
  const health = getDesktopHealth();
883
1101
  return jsonResponse({
884
- applied: savedFingerprint !== null,
885
- appliedAt,
1102
+ desiredEnabled,
1103
+ installed: observed.kind !== "not_installed",
1104
+ observedKind: observed.kind,
1105
+ applied,
1106
+ appliedAt: persisted.claudeCode?.desktopProfile?.appliedAt ?? null,
886
1107
  savedFingerprint,
887
- onDiskFingerprint,
888
- configPath,
1108
+ onDiskFingerprint: observed.fingerprint ?? null,
1109
+ configPath: observed.selectedProfilePath,
889
1110
  stale,
890
- activeProfile,
1111
+ // Tri-state by ID match, independent of profile health: null =
1112
+ // undeterminable (no/unreadable metadata or no appliedId); a readable
1113
+ // appliedId with no owned entry is a KNOWN false. Predates the inspector.
1114
+ activeProfile: observed.ownedProfileActive,
1115
+ drift: desiredEnabled ? !applied || stale : applied || observed.kind === "unsafe",
1116
+ driftReason: desiredEnabled ? (!applied ? "desired_on_not_current" : stale ? "profile_drift" : null) : (applied ? "desired_off_gateway_selected" : null),
891
1117
  health,
892
1118
  });
893
1119
  } catch (error) {
@@ -921,7 +1147,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
921
1147
  if (isDisabled(m.provider, m.id)) continue;
922
1148
  aliases.push({ id: claudeCodeAlias(m.provider, m.id), display_name: `${m.id} (${m.provider})` });
923
1149
  }
924
- const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models);
1150
+ const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models, nativeContextLimits(config));
925
1151
  const webSearchOverride = config.claudeCode?.webSearchSidecar;
926
1152
  const visionOverride = config.claudeCode?.visionSidecar;
927
1153
  // Auto is a RESOLUTION, recomputed per request — never stored state. Detection is
@@ -950,8 +1176,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
950
1176
  smallFastModel: config.claudeCode?.smallFastModel ?? "",
951
1177
  tierModels: config.claudeCode?.tierModels ?? {},
952
1178
  modelMap: config.claudeCode?.modelMap ?? {},
1179
+ classifierModel: config.claudeCode?.classifierModel ?? "",
1180
+ classifierFallbacks: config.claudeCode?.classifierFallbacks ?? [],
953
1181
  systemEnv: config.claudeCode?.systemEnv === true,
954
- autoConnectSupported: process.platform === "darwin",
1182
+ autoConnectSupported: (ctx.deps.platform ?? process.platform) === "darwin",
955
1183
  maxContextTokens: config.claudeCode?.maxContextTokens ?? null,
956
1184
  alwaysEnableEffort: config.claudeCode?.alwaysEnableEffort === true,
957
1185
  autoContext: config.claudeCode?.autoContext !== false,
@@ -987,18 +1215,83 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
987
1215
  return prototype === Object.prototype || prototype === null;
988
1216
  };
989
1217
  if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
990
- const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1218
+ const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
991
1219
  for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
992
1220
  const section = body[field];
993
1221
  if (section === undefined || section === null) continue;
994
1222
  if (!isPlainObject(section)) return jsonResponse({ error: `${field} must be an object or null` }, 400);
1223
+ // Both overrides now speak their full unions (roadmap 060 web, 170
1224
+ // vision revised). Vision's third arm is "routed" (loopback through the
1225
+ // proxy's own router), never exa: exa is not an LLM, and accepting an
1226
+ // unknown literal would persist a backend the vision resolver reads as
1227
+ // unset (review F1's failure mode).
1228
+ const allowedBackends = field === "webSearchSidecar"
1229
+ ? ["openai", "anthropic", "xai", "gemini", "exa"]
1230
+ : ["openai", "anthropic", "routed"];
995
1231
  if (section.backend !== undefined && section.backend !== null
996
- && section.backend !== "openai" && section.backend !== "anthropic") {
997
- return jsonResponse({ error: `${field}.backend must be openai, anthropic, or null` }, 400);
1232
+ && !allowedBackends.includes(section.backend as string)) {
1233
+ return jsonResponse({ error: `${field}.backend must be ${allowedBackends.join(", ")}, or null` }, 400);
998
1234
  }
999
1235
  if (section.model !== undefined && typeof section.model !== "string") {
1000
1236
  return jsonResponse({ error: `${field}.model must be a string` }, 400);
1001
1237
  }
1238
+ // Vision override only: reject a model we can prove is blind. Unknown ids stay
1239
+ // allowed. Shares one policy module with /api/sidecar-settings so the two
1240
+ // gates cannot drift.
1241
+ if (field === "visionSidecar" && typeof section.model === "string" && section.model !== "") {
1242
+ const requested = section.model;
1243
+ const candidates = await visionCandidateRows(config);
1244
+ const hint = section.backend === "anthropic" || section.backend === "openai"
1245
+ || section.backend === "routed"
1246
+ ? section.backend
1247
+ : config.claudeCode?.visionSidecar?.backend;
1248
+ // Same coherence rule as /api/sidecar-settings (roadmap 170 r2).
1249
+ const effectiveBackend = hint ?? "openai";
1250
+ const namespaced = requested.includes("/");
1251
+ if (namespaced && effectiveBackend !== "routed") {
1252
+ return jsonResponse({ error: `visionSidecar.model "${requested}" is provider-namespaced; it requires backend "routed"` }, 400);
1253
+ }
1254
+ if (!namespaced && effectiveBackend === "routed") {
1255
+ return jsonResponse({ error: `visionSidecar.backend "routed" requires a provider-namespaced model ("provider/model"); got "${requested}"` }, 400);
1256
+ }
1257
+ if (visionDescriberIsProvablyBlind(config, requested, candidates, hint)) {
1258
+ return jsonResponse(visionDescriberRejection("visionSidecar.model", requested, config, candidates), 400);
1259
+ }
1260
+ }
1261
+ // Web-search override: membership gate (#2188). The executor set is closed,
1262
+ // so an id outside (runnable candidates ∪ auth slots) can never run. Same
1263
+ // module as /api/sidecar-settings — a gate on one route and a stale copy on
1264
+ // the other is no gate at all.
1265
+ if (field === "webSearchSidecar"
1266
+ && (section.model !== undefined || section.backend !== undefined)) {
1267
+ const stored = config.claudeCode?.webSearchSidecar;
1268
+ // Validate against the SUBMITTED backend across the whole union, not
1269
+ // just openai/anthropic (#2457). allowedBackends above already refused
1270
+ // unknown literals; Array.includes does not narrow, hence the cast.
1271
+ // null keeps its own meaning here — drop the override and inherit the
1272
+ // global backend — which is deliberately NOT the sidecar-settings rule.
1273
+ const submittedBackend = section.backend;
1274
+ const effectiveBackend = typeof submittedBackend === "string"
1275
+ && allowedBackends.includes(submittedBackend)
1276
+ ? submittedBackend as WebSearchBackend
1277
+ : submittedBackend === null
1278
+ ? config.webSearchSidecar?.backend ?? "openai"
1279
+ : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1280
+ const effectiveModel = section.model === ""
1281
+ ? config.webSearchSidecar?.model
1282
+ : typeof section.model === "string"
1283
+ ? section.model
1284
+ : stored?.model ?? config.webSearchSidecar?.model;
1285
+ const candidates = await webSearchCandidateRows(config);
1286
+ if (effectiveModel && webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
1287
+ return jsonResponse(webSearchModelRejection(
1288
+ "webSearchSidecar.model",
1289
+ effectiveBackend,
1290
+ effectiveModel,
1291
+ candidates,
1292
+ ), 400);
1293
+ }
1294
+ }
1002
1295
  }
1003
1296
  const next = { ...(config.claudeCode ?? {}) };
1004
1297
  for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
@@ -1008,13 +1301,17 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1008
1301
  delete next[field];
1009
1302
  continue;
1010
1303
  }
1011
- const requested = section as { backend?: "openai" | "anthropic" | null; model?: string };
1012
- const override: NonNullable<OcxClaudeCodeConfig[typeof field]> = { ...next[field] };
1304
+ // The per-field validation above guarantees vision only ever carries the two-member
1305
+ // union; the cast is the loop's shared-shape compromise, not a wider write path.
1306
+ const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string };
1307
+ const override = { ...next[field] } as NonNullable<OcxClaudeCodeConfig[typeof field]>;
1013
1308
  if (requested.backend === null) delete override.backend;
1014
- else if (requested.backend !== undefined) override.backend = requested.backend;
1309
+ else if (requested.backend !== undefined) override.backend = requested.backend as never;
1015
1310
  if (requested.model === "") delete override.model;
1016
1311
  else if (requested.model !== undefined) override.model = requested.model;
1017
- if (Object.keys(override).length > 0) next[field] = override;
1312
+ // Indexed write across the field union collapses to an intersection; runtime
1313
+ // validation above already guarantees the per-field shape.
1314
+ if (Object.keys(override).length > 0) next[field] = override as never;
1018
1315
  else delete next[field];
1019
1316
  }
1020
1317
  if (body.enabled !== undefined) {
@@ -1107,19 +1404,38 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1107
1404
  else delete next.tierModels;
1108
1405
  }
1109
1406
  }
1407
+ let nextFastMode = config.fastMode;
1110
1408
  if (body.fastMode !== undefined) {
1111
1409
  if (body.fastMode !== true && body.fastMode !== false && body.fastMode !== null) {
1112
1410
  return jsonResponse({ error: "fastMode must be true, false, or null" }, 400);
1113
1411
  }
1114
- config.fastMode = body.fastMode === null ? undefined : body.fastMode;
1412
+ nextFastMode = body.fastMode === null ? undefined : body.fastMode;
1115
1413
  }
1116
- for (const field of ["model", "smallFastModel"] as const) {
1414
+ for (const field of ["model", "smallFastModel", "classifierModel"] as const) {
1117
1415
  const value = body[field];
1118
1416
  if (value === undefined) continue;
1119
1417
  if (typeof value !== "string") return jsonResponse({ error: `${field} must be a string` }, 400);
1120
1418
  if (value.trim() === "") delete next[field];
1121
1419
  else next[field] = value.trim();
1122
1420
  }
1421
+ if (body.classifierFallbacks !== undefined) {
1422
+ if (body.classifierFallbacks === null) {
1423
+ delete next.classifierFallbacks;
1424
+ } else {
1425
+ if (!Array.isArray(body.classifierFallbacks)) {
1426
+ return jsonResponse({ error: "classifierFallbacks must be an array of strings, or null" }, 400);
1427
+ }
1428
+ const list: string[] = [];
1429
+ for (const entry of body.classifierFallbacks) {
1430
+ if (typeof entry !== "string" || entry.trim() === "") {
1431
+ return jsonResponse({ error: "classifierFallbacks entries must be non-empty strings" }, 400);
1432
+ }
1433
+ list.push(entry.trim());
1434
+ }
1435
+ if (list.length > 0) next.classifierFallbacks = list;
1436
+ else delete next.classifierFallbacks;
1437
+ }
1438
+ }
1123
1439
  if (body.modelMap !== undefined) {
1124
1440
  if (body.modelMap === null) {
1125
1441
  delete next.modelMap;
@@ -1138,6 +1454,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1138
1454
  else delete next.modelMap;
1139
1455
  }
1140
1456
  }
1457
+ if (body.fastMode !== undefined) config.fastMode = nextFastMode;
1141
1458
  config.claudeCode = next;
1142
1459
  // Stamp the migration sentinel on EVERY persist of this block. The migration reads
1143
1460
  // "a claudeCode block with no authMode" as a pre-upgrade subscriber and pins it to