@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
@@ -15,21 +15,49 @@ import { CODEX_HOME, getCodexHome } from "./paths";
15
15
  import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
16
16
  import {
17
17
  canAcquireCodexQuotaProbeLease,
18
+ canAcquireCodexQuotaScopeProbeLease,
19
+ codexQuotaScopeForModel,
18
20
  computeCodexUsageScore,
21
+ getCodexQuotaHealthSnapshot,
22
+ getEffectiveActiveCodexAccountId,
19
23
  getPoolAccountPlan,
20
- isCodexAccountInCooldown,
21
24
  } from "./routing";
22
- import { isCodexAccountUsable } from "./account-usability";
25
+ import {
26
+ isCodexAccountUsable,
27
+ type CodexAccountUsabilityOptions,
28
+ } from "./account-usability";
23
29
  import { isCodexAccountPaused } from "./account-pause";
24
30
  import { slugEquals } from "../providers/slug-codec";
25
31
  import { isThreadSpawnRequest } from "../server/effort-policy";
26
32
  import { PROVIDER_REGISTRY } from "../providers/registry";
27
- import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
33
+ import {
34
+ CODEX_FORWARD_BASE_URL,
35
+ OPENAI_CODEX_PROVIDER_ID,
36
+ isCanonicalOpenAiForwardProvider,
37
+ } from "../providers/openai-tiers";
28
38
  import { routeModel, type RouteResult } from "../router";
29
39
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
40
+ import { codexAccountNamespaceForModel } from "./account-namespace-match";
41
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
42
+ import {
43
+ getUpstreamHostHealth,
44
+ normalizeUpstreamHostCircuitThreshold,
45
+ upstreamHostHealthKey,
46
+ } from "./upstream-host-health";
30
47
  export const DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS = 60_000;
31
48
 
49
+ const CODEX_FORWARD_ORIGIN = new URL(CODEX_FORWARD_BASE_URL).origin.toLowerCase();
50
+
32
51
  type SubagentQuotaPrimeFn = (config: OcxConfig, reason: string) => Promise<void>;
52
+ /** Side-effect-free Pool account preview for one resolved fallback candidate. */
53
+ export type SubagentPoolAccountPreview = (
54
+ modelId: string | undefined,
55
+ now: number,
56
+ modelEligibleAccountIds?: ReadonlySet<string>,
57
+ ) => string | null;
58
+ export type SubagentModelEligibleAccountIds = (
59
+ modelId: string | undefined,
60
+ ) => ReadonlySet<string> | undefined;
33
61
  let subagentQuotaPrimeForTests: SubagentQuotaPrimeFn | null = null;
34
62
  let quotaPrimeInFlight: Promise<void> | null = null;
35
63
 
@@ -68,6 +96,13 @@ function isDisabledFallbackModel(model: string, config: OcxConfig): boolean {
68
96
  const slash = model.indexOf("/");
69
97
  const provider = model.slice(0, slash);
70
98
  const modelId = model.slice(slash + 1);
99
+ if (codexAccountNamespaceForModel(config.codexAccountNamespaces, model)) {
100
+ return disabled.some(stored =>
101
+ stored === model
102
+ || stored === modelId
103
+ || slugEquals(stored, "openai", modelId)
104
+ );
105
+ }
71
106
  return disabled.some(stored => stored === model || slugEquals(stored, provider, modelId));
72
107
  }
73
108
 
@@ -79,13 +114,27 @@ function pollIntervalMs(config: OcxConfig): number {
79
114
  return configured;
80
115
  }
81
116
 
82
- function normalizedChain(primary: string, config: OcxConfig, extra: readonly string[] = []): string[] {
117
+ function fallbackChainKey(model: string, namespaces: unknown): string {
118
+ const selector = codexAccountNamespaceForModel(namespaces, model);
119
+ if (!selector) return JSON.stringify(["model", model.toLowerCase()]);
120
+ const slash = model.indexOf("/");
121
+ // Selector keys are exact-case account boundaries. Keep that segment distinct while
122
+ // retaining legacy case-insensitive de-duplication for the native model suffix.
123
+ return JSON.stringify(["account", selector, model.slice(slash + 1).toLowerCase()]);
124
+ }
125
+
126
+ function normalizedChain(
127
+ primary: string,
128
+ config: OcxConfig,
129
+ extra: readonly string[] = [],
130
+ trailing: readonly string[] = [],
131
+ ): string[] {
83
132
  const chain: string[] = [];
84
133
  const seen = new Set<string>();
85
134
  const push = (model: string | undefined) => {
86
135
  if (!model || model.trim() === "") return;
87
136
  const trimmed = model.trim();
88
- const key = trimmed.toLowerCase();
137
+ const key = fallbackChainKey(trimmed, config.codexAccountNamespaces);
89
138
  if (seen.has(key)) return;
90
139
  seen.add(key);
91
140
  chain.push(trimmed);
@@ -93,6 +142,7 @@ function normalizedChain(primary: string, config: OcxConfig, extra: readonly str
93
142
  push(primary);
94
143
  for (const model of extra) push(model);
95
144
  for (const model of config.subagentModelFallback ?? []) push(model);
145
+ for (const model of trailing) push(model);
96
146
  return chain;
97
147
  }
98
148
 
@@ -109,8 +159,14 @@ function quotaThreshold(config: OcxConfig): number {
109
159
  return threshold > 0 ? threshold : Number.POSITIVE_INFINITY;
110
160
  }
111
161
 
162
+ /**
163
+ * The account routing would actually use, not just the persisted operator
164
+ * selection: round-robin, fill-first, failover, and priority preemption all
165
+ * move the cursor in memory only, so reading the raw field would check quota
166
+ * against an account this request is not going to touch.
167
+ */
112
168
  function activeCodexAccountId(config: OcxConfig): string | null {
113
- return config.activeCodexAccountId ?? null;
169
+ return getEffectiveActiveCodexAccountId(config) ?? null;
114
170
  }
115
171
 
116
172
  /**
@@ -127,9 +183,25 @@ function resolvePoolFallbackAccountId(
127
183
  return activeCodexAccountId(config);
128
184
  }
129
185
 
186
+ function resolveRouteFallbackAccountId(
187
+ route: RouteResult | null,
188
+ config: OcxConfig,
189
+ accountId?: string | null,
190
+ now = Date.now(),
191
+ poolAccountPreview?: SubagentPoolAccountPreview,
192
+ modelEligibleAccountIds?: ReadonlySet<string>,
193
+ ): string | null {
194
+ if (route?.codexAccountId !== undefined) return route.codexAccountId;
195
+ if (route && isPoolCodexRoute(route) && poolAccountPreview) {
196
+ return poolAccountPreview(route.modelId, now, modelEligibleAccountIds);
197
+ }
198
+ return resolvePoolFallbackAccountId(config, accountId);
199
+ }
200
+
130
201
  function isRoutableFallbackModel(model: string, config: OcxConfig): boolean {
131
202
  const slash = model.indexOf("/");
132
203
  if (slash > 0) {
204
+ if (codexAccountNamespaceForModel(config.codexAccountNamespaces, model)) return true;
133
205
  const providerName = model.slice(0, slash);
134
206
  if (!hasOwnProvider(config.providers, providerName)) {
135
207
  // Allow well-known "vendor/model" ids (e.g. anthropic/claude-*) to flow as
@@ -150,7 +222,7 @@ export function isNativeModelQuotaExhausted(
150
222
  ): boolean {
151
223
  const route = tryRouteFallbackModel(config, model);
152
224
  if (!route || !isPoolCodexRoute(route)) return false;
153
- const resolvedAccountId = resolvePoolFallbackAccountId(config, accountId);
225
+ const resolvedAccountId = resolveRouteFallbackAccountId(route, config, accountId);
154
226
  if (!resolvedAccountId) return false;
155
227
  const quota = getAccountQuota(resolvedAccountId);
156
228
  const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId));
@@ -167,7 +239,11 @@ export function isModelHealthBlocked(
167
239
  const route = tryRouteFallbackModel(config, model);
168
240
  const poolScoped = !!route && isPoolCodexRoute(route);
169
241
  const health = modelHealth.get(
170
- healthKey(model, resolvePoolFallbackAccountId(config, accountId), poolScoped),
242
+ healthKey(
243
+ model,
244
+ resolveRouteFallbackAccountId(route, config, accountId),
245
+ poolScoped,
246
+ ),
171
247
  );
172
248
  return !!health && health.unavailableUntil > now;
173
249
  }
@@ -177,27 +253,54 @@ export function isSubagentModelUnavailable(
177
253
  config: OcxConfig,
178
254
  accountId?: string | null,
179
255
  now = Date.now(),
256
+ accountUsabilityOptions?: CodexAccountUsabilityOptions,
257
+ poolAccountPreview?: SubagentPoolAccountPreview,
258
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
180
259
  ): boolean {
181
260
  if (isDisabledFallbackModel(model, config)) return true;
182
261
  if (!isRoutableFallbackModel(model, config)) return true;
183
262
  const route = tryRouteFallbackModel(config, model);
184
263
  if (!route || route.provider.disabled === true) return true;
185
- if (isModelHealthBlocked(model, config, accountId, now)) return true;
264
+ const modelEligibleAccountIds = modelEligibleAccountIdsForModel?.(route.modelId);
265
+ const candidateAccountUsabilityOptions = modelEligibleAccountIds !== undefined
266
+ ? {
267
+ ...accountUsabilityOptions,
268
+ modelEligibleAccountIds,
269
+ }
270
+ : accountUsabilityOptions;
271
+ const resolvedAccountId = resolveRouteFallbackAccountId(
272
+ route,
273
+ config,
274
+ accountId,
275
+ now,
276
+ poolAccountPreview,
277
+ candidateAccountUsabilityOptions?.modelEligibleAccountIds,
278
+ );
279
+ if (isModelHealthBlocked(model, config, resolvedAccountId, now)) return true;
186
280
  if (!isPoolCodexRoute(route)) return false;
187
281
 
188
282
  // Pool candidates need a usable account. Derive requirement from the resolved
189
283
  // route (canonical openai defaults to pool even when codexAccountMode is omitted).
190
- const resolvedAccountId = resolvePoolFallbackAccountId(config, accountId);
191
284
  if (!resolvedAccountId) return true;
192
285
  if (isCodexAccountPaused(config, resolvedAccountId)) return true;
193
- if (!isCodexAccountUsable(config, resolvedAccountId)) return true;
194
- if (
195
- isCodexAccountInCooldown(resolvedAccountId, now)
196
- && !canAcquireCodexQuotaProbeLease(resolvedAccountId, now)
197
- ) {
198
- return true;
286
+ if (!isCodexAccountUsable(config, resolvedAccountId, candidateAccountUsabilityOptions)) return true;
287
+ if (route.codexAccountId !== undefined) {
288
+ // An account-qualified route is pinned and cannot consume Pool's recovery-probe
289
+ // escape hatch. Honor both account-wide and model-scoped cooldowns so fallback
290
+ // advances instead of selecting a candidate that exact auth will reject.
291
+ const quotaScope = codexQuotaScopeForModel(route.modelId);
292
+ if (getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now) !== null) return true;
293
+ } else {
294
+ const quotaScope = codexQuotaScopeForModel(route.modelId);
295
+ const cooldown = getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now);
296
+ if (cooldown !== null) {
297
+ const probeAvailable = cooldown.quotaScope
298
+ ? canAcquireCodexQuotaScopeProbeLease(resolvedAccountId, cooldown.quotaScope, now)
299
+ : canAcquireCodexQuotaProbeLease(resolvedAccountId, now);
300
+ if (!probeAvailable) return true;
301
+ }
199
302
  }
200
- return isNativeModelQuotaExhausted(model, config, accountId, now);
303
+ return isNativeModelQuotaExhausted(model, config, resolvedAccountId, now);
201
304
  }
202
305
 
203
306
  export function selectAvailableSubagentModel(
@@ -207,8 +310,13 @@ export function selectAvailableSubagentModel(
207
310
  accountId?: string | null,
208
311
  now = Date.now(),
209
312
  nativeFallbackOnly = false,
313
+ accountUsabilityOptions?: CodexAccountUsabilityOptions,
314
+ trailingFallback: readonly string[] = [],
315
+ poolAccountPreview?: SubagentPoolAccountPreview,
316
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
317
+ resolvedChain?: readonly string[],
210
318
  ): { model: string; rewritten: boolean; skipped: string[] } {
211
- const chain = normalizedChain(primary, config, extraFallback);
319
+ const chain = resolvedChain ?? normalizedChain(primary, config, extraFallback, trailingFallback);
212
320
  const skipped: string[] = [];
213
321
  for (const candidate of chain) {
214
322
  if (nativeFallbackOnly) {
@@ -218,7 +326,15 @@ export function selectAvailableSubagentModel(
218
326
  continue;
219
327
  }
220
328
  }
221
- if (isSubagentModelUnavailable(candidate, config, accountId, now)) {
329
+ if (isSubagentModelUnavailable(
330
+ candidate,
331
+ config,
332
+ accountId,
333
+ now,
334
+ accountUsabilityOptions,
335
+ poolAccountPreview,
336
+ modelEligibleAccountIdsForModel,
337
+ )) {
222
338
  skipped.push(candidate);
223
339
  continue;
224
340
  }
@@ -240,7 +356,11 @@ export function noteSubagentModelFailure(
240
356
  const route = tryRouteFallbackModel(config, model);
241
357
  const poolScoped = !!route && isPoolCodexRoute(route);
242
358
  modelHealth.set(
243
- healthKey(model, resolvePoolFallbackAccountId(config, accountId), poolScoped),
359
+ healthKey(
360
+ model,
361
+ resolveRouteFallbackAccountId(route, config, accountId),
362
+ poolScoped,
363
+ ),
244
364
  {
245
365
  unavailableUntil: now + interval,
246
366
  reason: "quota_exhausted",
@@ -324,13 +444,16 @@ export function readCodexAgentModel(role: string, codexHome = CODEX_HOME): strin
324
444
  export function resolveAgentModelFallbackForPrimary(
325
445
  primary: string,
326
446
  codexHome = CODEX_HOME,
447
+ namespaces?: unknown,
327
448
  ): string[] {
328
449
  const merged: string[] = [];
329
450
  const seen = new Set<string>();
330
451
  const push = (model: string | null | undefined) => {
331
452
  if (!model || model.trim() === "") return;
332
453
  const trimmed = model.trim();
333
- const key = trimmed.toLowerCase();
454
+ // Match global-chain de-dupe: keep account-selector prefixes case-sensitive when
455
+ // those selectors are configured, while ordinary provider/model ids stay case-insensitive.
456
+ const key = fallbackChainKey(trimmed, namespaces);
334
457
  if (seen.has(key)) return;
335
458
  seen.add(key);
336
459
  merged.push(trimmed);
@@ -343,18 +466,66 @@ export function resolveAgentModelFallbackForPrimary(
343
466
  return merged;
344
467
  }
345
468
 
469
+ function subagentQuotaPrimeBlockedByHostCircuit(config: OcxConfig): boolean {
470
+ if (normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) === 0) return false;
471
+ const key = upstreamHostHealthKey(OPENAI_CODEX_PROVIDER_ID, CODEX_FORWARD_ORIGIN);
472
+ return getUpstreamHostHealth(key)?.cooldownUntil !== undefined;
473
+ }
474
+
475
+ /**
476
+ * Per-primary-model fallback chains from opencodex config (#1190).
477
+ *
478
+ * Storing `model_fallback` inside `$CODEX_HOME/agents/*.toml` makes Codex >= 0.146
479
+ * reject the whole role file as an unknown field. The config-keyed map is the
480
+ * supported home for that metadata; keys match the requested primary model id,
481
+ * using the same raw/encoded slug tolerance as the TOML role lookup.
482
+ */
483
+ export function resolveConfiguredModelFallbackForPrimary(
484
+ primary: string,
485
+ config: OcxConfig,
486
+ ): string[] {
487
+ const byModel = config.subagentModelFallbackByModel;
488
+ if (!byModel || typeof byModel !== "object") return [];
489
+ const entries: string[] = [];
490
+ const seen = new Set<string>();
491
+ const push = (model: string) => {
492
+ const trimmed = model.trim();
493
+ if (trimmed === "") return;
494
+ const key = fallbackChainKey(trimmed, config.codexAccountNamespaces);
495
+ if (seen.has(key)) return;
496
+ seen.add(key);
497
+ entries.push(trimmed);
498
+ };
499
+ for (const [key, chain] of Object.entries(byModel)) {
500
+ if (!slugsEquivalent(key, primary)) continue;
501
+ for (const model of chain) push(model);
502
+ }
503
+ return entries;
504
+ }
505
+
346
506
  /**
347
507
  * Best-effort quota refresh before subagent model selection.
348
508
  * Concurrent callers share one in-flight promise. The success TTL is updated only
349
509
  * after a successful refresh so failures remain retryable. Errors are swallowed so
350
- * spawn routing can continue.
510
+ * spawn routing can continue. When the canonical ChatGPT origin is circuit-blocked,
511
+ * cached quota is used instead of sending credential-bearing usage probes to the
512
+ * same origin before the request's final host admission check.
351
513
  */
352
- export function maybePrimeSubagentQuota(config: OcxConfig, now = Date.now()): Promise<void> {
514
+ export function maybePrimeSubagentQuota(
515
+ config: OcxConfig,
516
+ now = Date.now(),
517
+ options: { nativeMainReadsForbidden?: boolean } = {},
518
+ ): Promise<void> {
519
+ if (options.nativeMainReadsForbidden) return Promise.resolve();
520
+ if (subagentQuotaPrimeBlockedByHostCircuit(config)) return Promise.resolve();
353
521
  if (quotaPrimeInFlight) return quotaPrimeInFlight;
354
522
  if (!shouldPrimeSubagentQuota(config, now)) return Promise.resolve();
355
523
 
356
524
  quotaPrimeInFlight = (async () => {
357
525
  try {
526
+ // Re-check after claiming single-flight ownership so a circuit opened by
527
+ // a concurrent request cannot race us into a fresh usage-probe pass.
528
+ if (subagentQuotaPrimeBlockedByHostCircuit(config)) return;
358
529
  if (subagentQuotaPrimeForTests) {
359
530
  await subagentQuotaPrimeForTests(config, "subagent-spawn");
360
531
  } else {
@@ -391,18 +562,28 @@ export function applySubagentModelFallback(
391
562
  accountId?: string | null,
392
563
  now = Date.now(),
393
564
  nativeFallbackOnly = false,
565
+ accountUsabilityOptions?: CodexAccountUsabilityOptions,
566
+ poolAccountPreview?: SubagentPoolAccountPreview,
567
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
568
+ resolvedFallbackChain?: readonly string[] | null,
394
569
  ): { from?: string; to?: string; skipped?: string[] } | null {
395
570
  if (!isThreadSpawnRequest(headers)) return null;
396
- const roleFallback = resolveAgentModelFallbackForPrimary(parsed.modelId, getCodexHome());
397
- const globalFallback = config.subagentModelFallback ?? [];
398
- if (globalFallback.length === 0 && roleFallback.length === 0) return null;
571
+ const fallbackChain = resolvedFallbackChain === undefined
572
+ ? resolveSubagentFallbackChain(parsed, config)
573
+ : resolvedFallbackChain;
574
+ if (!fallbackChain) return null;
399
575
  const selection = selectAvailableSubagentModel(
400
576
  parsed.modelId,
401
577
  config,
402
- roleFallback,
578
+ [],
403
579
  accountId,
404
580
  now,
405
581
  nativeFallbackOnly,
582
+ accountUsabilityOptions,
583
+ [],
584
+ poolAccountPreview,
585
+ modelEligibleAccountIdsForModel,
586
+ fallbackChain,
406
587
  );
407
588
  if (!selection.rewritten) return selection.skipped.length > 0
408
589
  ? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
@@ -412,6 +593,37 @@ export function applySubagentModelFallback(
412
593
  return { from, to: selection.model, skipped: selection.skipped };
413
594
  }
414
595
 
596
+ /** Resolve the effective fallback chain once for one logical spawn request. */
597
+ export function resolveSubagentFallbackChain(
598
+ parsed: OcxParsedRequest,
599
+ config: OcxConfig,
600
+ ): readonly string[] | null {
601
+ const tomlRoleFallback = resolveAgentModelFallbackForPrimary(
602
+ parsed.modelId,
603
+ getCodexHome(),
604
+ config.codexAccountNamespaces,
605
+ );
606
+ // Config-keyed chains are the supported per-role home (#1190); TOML `model_fallback`
607
+ // stays readable for backwards compatibility with homes written before Codex 0.146.
608
+ const configuredFallback = resolveConfiguredModelFallbackForPrimary(parsed.modelId, config);
609
+ const globalFallback = config.subagentModelFallback ?? [];
610
+ if (globalFallback.length === 0 && configuredFallback.length === 0 && tomlRoleFallback.length === 0) return null;
611
+ return normalizedChain(parsed.modelId, config, configuredFallback, tomlRoleFallback);
612
+ }
613
+
614
+ /** Whether the effective fallback chain crosses an account-gated native Pool model. */
615
+ export function subagentFallbackNeedsModelEntitlements(
616
+ fallbackChain: readonly string[] | null,
617
+ config: OcxConfig,
618
+ ): boolean {
619
+ return fallbackChain?.some((candidate) => {
620
+ const route = tryRouteFallbackModel(config, candidate);
621
+ return !!route
622
+ && isPoolCodexRoute(route)
623
+ && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId);
624
+ }) === true;
625
+ }
626
+
415
627
  export function subagentFallbackGuidanceText(config: OcxConfig): string {
416
628
  const chain = config.subagentModelFallback ?? [];
417
629
  if (chain.length === 0) return "";
@@ -419,33 +631,192 @@ export function subagentFallbackGuidanceText(config: OcxConfig): string {
419
631
  return ` Subagent model fallback chain (priority order): ${quoted}. When the primary model is quota-exhausted, opencodex rewrites thread_spawn requests to the next available model automatically.`;
420
632
  }
421
633
 
422
- const TOML_STRING_ARRAY = /^(model_fallback)\s*=\s*\[(.*)\]\s*$/s;
634
+ const TOML_MODEL_FALLBACK_KEY = /^\s*(?:model_fallback|"model_fallback"|'model_fallback')\s*=/;
635
+
636
+ type TomlModelFallbackField = { present: false; value: null } | { present: true; value: string[] | null };
423
637
 
424
- function parseTomlStringArray(raw: string): string[] {
425
- const matches = [...raw.matchAll(/"((?:\\.|[^"\\])*)"/g)];
426
- return matches.map(match => match[1]!.replace(/\\"/g, "\""));
638
+ type TomlScanState = {
639
+ inMultilineString: '"""' | "'''" | null;
640
+ arrayDepth: number;
641
+ };
642
+
643
+ /** Track TOML strings, comments, and array brackets on one line. */
644
+ function scanTomlLine(line: string, state: TomlScanState): void {
645
+ let i = 0;
646
+ while (i < line.length) {
647
+ const ch = line[i]!;
648
+ if (ch === "#") return;
649
+ if (ch === '"' || ch === "'") {
650
+ const delimiter = ch.repeat(3);
651
+ if (line.startsWith(delimiter, i)) {
652
+ const end = findTomlMultilineStringEnd(line, i + 3, ch);
653
+ if (end === -1) {
654
+ state.inMultilineString = delimiter as '"""' | "'''";
655
+ return;
656
+ }
657
+ i = end + 3;
658
+ continue;
659
+ }
660
+ i++;
661
+ while (i < line.length) {
662
+ if (ch === '"' && line[i] === "\\") {
663
+ i += 2;
664
+ continue;
665
+ }
666
+ if (line[i] === ch) break;
667
+ i++;
668
+ }
669
+ i++;
670
+ continue;
671
+ }
672
+ if (ch === "[") state.arrayDepth++;
673
+ else if (ch === "]") state.arrayDepth = Math.max(0, state.arrayDepth - 1);
674
+ i++;
675
+ }
427
676
  }
428
677
 
429
- function parseTomlModelFallback(content: string): string[] | null {
430
- const match = content.match(/^\s*model_fallback\s*=\s*\[(.*?)\]\s*$/ms);
431
- if (!match) return null;
432
- return parseTomlStringArray(match[1] ?? "");
678
+ /** Parse a TOML string starting at `start`; returns the decoded value and end offset. */
679
+ function parseTomlStringAt(text: string, start: number): { value: string; end: number } | null {
680
+ const quote = text[start]!;
681
+ const delimiter = quote.repeat(3);
682
+ if (text.startsWith(delimiter, start)) {
683
+ const end = findTomlMultilineStringEnd(text, start + 3, quote);
684
+ if (end === -1) return null;
685
+ let value = text.slice(start + 3, end).trim();
686
+ if (quote === '"') value = value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
687
+ return { value, end: end + 3 };
688
+ }
689
+ let i = start + 1;
690
+ let value = "";
691
+ while (i < text.length) {
692
+ const ch = text[i]!;
693
+ if (quote === '"' && ch === "\\") {
694
+ const next = text[i + 1];
695
+ if (next === '"' || next === "\\") {
696
+ value += next;
697
+ i += 2;
698
+ continue;
699
+ }
700
+ }
701
+ if (ch === quote) return { value, end: i + 1 };
702
+ value += ch;
703
+ i++;
704
+ }
705
+ return null;
706
+ }
707
+
708
+ function findTomlMultilineStringEnd(text: string, from: number, quote: string): number {
709
+ const delimiter = quote.repeat(3);
710
+ let index = text.indexOf(delimiter, from);
711
+ while (index !== -1) {
712
+ if (quote === "'") return index;
713
+ let backslashes = 0;
714
+ for (let j = index - 1; j >= 0 && text[j] === "\\"; j--) backslashes += 1;
715
+ if (backslashes % 2 === 0) return index;
716
+ index = text.indexOf(delimiter, index + 1);
717
+ }
718
+ return -1;
719
+ }
720
+
721
+ /** After an array close, only horizontal whitespace, an inline comment, or the line end is valid. */
722
+ function isValidTomlArrayTail(text: string, from: number): boolean {
723
+ let i = from;
724
+ while (i < text.length && (text[i] === " " || text[i] === "\t")) i += 1;
725
+ if (i >= text.length || text[i] === "\n" || text[i] === "\r") return true;
726
+ if (text[i] === "#") {
727
+ while (i < text.length && text[i] !== "\n") i += 1;
728
+ return true;
729
+ }
730
+ return false;
731
+ }
732
+
733
+ /** Parse a TOML string-array value; null when the value is not a well-formed array of strings. */
734
+ function parseTomlStringArrayValue(text: string): string[] | null {
735
+ const values: string[] = [];
736
+ let i = 0;
737
+ const skipIgnored = () => {
738
+ while (i < text.length) {
739
+ const ch = text[i]!;
740
+ if (ch === "#") {
741
+ while (i < text.length && text[i] !== "\n") i += 1;
742
+ continue;
743
+ }
744
+ if (/\s/.test(ch)) {
745
+ i += 1;
746
+ continue;
747
+ }
748
+ break;
749
+ }
750
+ };
751
+ skipIgnored();
752
+ if (text[i] !== "[") return null;
753
+ i += 1;
754
+ let expectValue = true;
755
+ for (;;) {
756
+ skipIgnored();
757
+ if (i >= text.length) return null;
758
+ const ch = text[i]!;
759
+ if (ch === "]") {
760
+ if (!isValidTomlArrayTail(text, i + 1)) return null;
761
+ return values;
762
+ }
763
+ if (ch === ",") {
764
+ if (expectValue) return null; // leading or doubled comma
765
+ expectValue = true;
766
+ i += 1;
767
+ continue;
768
+ }
769
+ if (ch === '"' || ch === "'") {
770
+ if (!expectValue) return null; // adjacent strings must be comma-separated
771
+ const parsed = parseTomlStringAt(text, i);
772
+ if (!parsed) return null;
773
+ values.push(parsed.value);
774
+ i = parsed.end;
775
+ expectValue = false;
776
+ continue;
777
+ }
778
+ return null;
779
+ }
780
+ }
781
+
782
+ /**
783
+ * TOML-aware, presence-aware parse of the `model_fallback` field. Quoted keys
784
+ * are recognized, and text inside strings (including multiline strings) is
785
+ * never treated as a key. `present` is true whenever the key exists, even when
786
+ * the value is not a readable string array; `value` is null in that case.
787
+ */
788
+ function parseTomlModelFallbackField(content: string): TomlModelFallbackField {
789
+ const lines = content.split(/\r?\n/);
790
+ const state: TomlScanState = { inMultilineString: null, arrayDepth: 0 };
791
+ for (let i = 0; i < lines.length; i += 1) {
792
+ const line = lines[i]!;
793
+ if (state.inMultilineString) {
794
+ const end = findTomlMultilineStringEnd(line, 0, state.inMultilineString[0]!);
795
+ if (end === -1) continue;
796
+ state.inMultilineString = null;
797
+ scanTomlLine(line.slice(end + 3), state);
798
+ continue;
799
+ }
800
+ if (state.arrayDepth === 0) {
801
+ const key = line.match(TOML_MODEL_FALLBACK_KEY);
802
+ if (key) {
803
+ const rest = `${line.slice(key[0].length)}\n${lines.slice(i + 1).join("\n")}`;
804
+ return { present: true, value: parseTomlStringArrayValue(rest) };
805
+ }
806
+ }
807
+ scanTomlLine(line, state);
808
+ }
809
+ return { present: false, value: null };
433
810
  }
434
811
 
435
812
  export function readAgentModelFallback(filePath: string): string[] | null {
436
813
  try {
437
814
  const content = readFileSync(filePath, "utf8");
438
- const multiline = parseTomlModelFallback(content);
439
- if (multiline) return multiline;
440
- for (const line of content.split(/\r?\n/)) {
441
- const match = line.match(TOML_STRING_ARRAY);
442
- if (!match) continue;
443
- return parseTomlStringArray(match[2] ?? "");
444
- }
815
+ const parsed = parseTomlModelFallbackField(content);
816
+ return parsed.present ? parsed.value : null;
445
817
  } catch {
446
818
  return null;
447
819
  }
448
- return null;
449
820
  }
450
821
 
451
822
  export function readCodexAgentModelFallback(role: string, codexHome = CODEX_HOME): string[] {
@@ -454,6 +825,29 @@ export function readCodexAgentModelFallback(role: string, codexHome = CODEX_HOME
454
825
  return readAgentModelFallback(file) ?? [];
455
826
  }
456
827
 
828
+ /**
829
+ * True when the role TOML carries a `model_fallback` key, even an empty array.
830
+ * Presence is what matters for the doctor scan: Codex >= 0.146 rejects the
831
+ * field as unknown and skips the whole role regardless of its value. Uses the
832
+ * same TOML-aware parse as the fallback-reading path, so quoted keys count and
833
+ * text inside string literals does not.
834
+ */
835
+ export function hasCodexAgentModelFallbackField(role: string, codexHome = CODEX_HOME): boolean {
836
+ const file = join(codexHome, "agents", `${role}.toml`);
837
+ if (!existsSync(file)) return false;
838
+ try {
839
+ const content = readFileSync(file, "utf8");
840
+ return parseTomlModelFallbackField(content).present;
841
+ } catch {
842
+ return false;
843
+ }
844
+ }
845
+
846
+ /** Roles whose TOML still carries `model_fallback`, including empty arrays. */
847
+ export function scanCodexAgentRolesWithTomlModelFallback(codexHome = CODEX_HOME): string[] {
848
+ return listCodexAgentRoles(codexHome).filter(role => hasCodexAgentModelFallbackField(role, codexHome));
849
+ }
850
+
457
851
  export function listCodexAgentRoles(codexHome = CODEX_HOME): string[] {
458
852
  const dir = join(codexHome, "agents");
459
853
  if (!existsSync(dir)) return [];