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