@yansigit/opencodex 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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1566 @@
1
+ /**
2
+ * features.ts — codex feature-flag view for $CODEX_HOME/config.toml.
3
+ *
4
+ * Scope boundary: this module mirrors only the flags opencodex has to READ
5
+ * directly from config.toml:
6
+ * - `multi_agent_v2`, because opencodex migrates its concurrency value across
7
+ * the v1/v2 boundary and exposes the multi-agent config surface;
8
+ * - `default_mode_request_user_input` (Codex Auth page toggle), because the
9
+ * management API needs a live reader for the flag it manages.
10
+ * Every other upstream feature flag is delegated to the native `codex features`
11
+ * command (see src/cli/v2.ts) and must not be hardcoded here.
12
+ *
13
+ * Upstream reshapes flags freely: in the 1f0566d3f..5a1097ed2 range alone,
14
+ * `code_mode_host` changed from a boolean to a table (it is Stage::Stable and
15
+ * default-enabled upstream), `enable_fanout` and `item_ids` were retired to
16
+ * Stage::Removed ("useless but kept for backward compatibility"), and several
17
+ * under-development flags were added. Delegation is what keeps opencodex out of
18
+ * that churn.
19
+ *
20
+ * Used by the catalog v2-gated-ultra policy (devlog/260709_v2_gated_ultra) and the
21
+ * `ocx v2` toggle surface. The FLAG itself is never written here — toggling goes
22
+ * through the official `codex features enable|disable` CLI (format-preserving).
23
+ * The one write this module owns is the numeric
24
+ * `features.multi_agent_v2.max_concurrent_threads_per_session` scalar
25
+ * (setMaxConcurrentThreads): the codex CLI has no persisted setter for nested
26
+ * feature config (`-c` is per-invocation only), so ocx does a scoped,
27
+ * EOL-preserving line edit — same practice as codex/inject.ts.
28
+ *
29
+ * CODEX_HOME is resolved at CALL time (activeCodexConfigPath pattern, mirrors
30
+ * catalog.ts:40-54) so tests can point fixtures via env or the explicit
31
+ * `configPath` parameter without fighting the module-load-time const in paths.ts.
32
+ */
33
+ import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
34
+ import { delimiter, dirname, join, resolve } from "node:path";
35
+ import { createRequire } from "node:module";
36
+ import { realpathSync } from "node:fs";
37
+ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWriteFile, expandUserPath, getConfigDir } from "../config";
38
+ import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
39
+ import { CODEX_CONFIG_PATH } from "./paths";
40
+ import { resolveAndPersistCodexRuntime } from "./runtime";
41
+
42
+ /** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
43
+ export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input";
44
+
45
+ // EOL preservation, local copies of inject.ts dominantEol/applyEol: importing
46
+ // inject here would close a module cycle (features -> inject -> catalog -> features).
47
+ function dominantEol(content: string): "\r\n" | "\n" {
48
+ const crlf = (content.match(/\r\n/g) ?? []).length;
49
+ if (crlf === 0) return "\n";
50
+ const bareLf = (content.match(/\n/g) ?? []).length - crlf;
51
+ return crlf >= bareLf ? "\r\n" : "\n";
52
+ }
53
+
54
+ function applyEol(content: string, eol: "\r\n" | "\n"): string {
55
+ const normalized = content.replace(/\r\n/g, "\n");
56
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
57
+ }
58
+
59
+ function mergeTrailingComments(existing?: string, migrated?: string): string {
60
+ if (!existing) return migrated ?? "";
61
+ if (!migrated || existing.trim() === migrated.trim()) return existing;
62
+ const migratedText = migrated.replace(/^\s*#\s*/, "");
63
+ if (existing.replace(/^\s*#\s*/, "").split(";").map(part => part.trim()).includes(migratedText.trim())) return existing;
64
+ return `${existing}; ${migratedText}`;
65
+ }
66
+
67
+ export function activeCodexConfigPath(): string {
68
+ const raw = process.env.CODEX_HOME?.trim();
69
+ if (!raw) return CODEX_CONFIG_PATH;
70
+ const path = resolve(expandUserPath(raw));
71
+ try {
72
+ return join(realpathSync.native(path), "config.toml");
73
+ } catch {
74
+ return join(path, "config.toml");
75
+ }
76
+ }
77
+
78
+ function readConfigText(configPath?: string): string | null {
79
+ const path = configPath ?? activeCodexConfigPath();
80
+ try {
81
+ if (!existsSync(path)) return null;
82
+ return readFileSync(path, "utf8");
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Body lines of a TOML table `[header]` up to (not including) the next table header.
90
+ *
91
+ * The implementation body is deliberately unchanged by #1295 — only this comment
92
+ * is new. The scanner is line-based and string-unaware, so it ends the table at
93
+ * the first line matching `/^\s*\[/` even inside a multi-line value. Twenty call
94
+ * sites in this file consume its output, most of them by matching a regex
95
+ * against the returned text, so widening that text changes what they match. An
96
+ * earlier attempt at #1295 made this scanner string-aware and thereby gave
97
+ * `getAgentsEnabled`, `getAgentsMaxDepth`, and `getMaxConcurrentThreads` three
98
+ * new wrong answers.
99
+ *
100
+ * The readers that matter for #1295 use a real TOML parse instead (see
101
+ * `parsedTomlTable`). This stays as the fallback for documents that do not
102
+ * parse, and as the reader for the remaining call sites until they are migrated
103
+ * the same way.
104
+ */
105
+ function tomlTableBody(content: string, header: string): string | null {
106
+ const lines = content.split("\n");
107
+ const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
+ const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
109
+ if (start === -1) return null;
110
+ const rest = lines.slice(start + 1);
111
+ const end = rest.findIndex(l => /^\s*\[/.test(l));
112
+ return (end === -1 ? rest : rest.slice(0, end)).join("\n");
113
+ }
114
+
115
+ function tomlBoolInBody(body: string, key: string): boolean | null {
116
+ const m = body.match(new RegExp(`^\\s*${key}\\s*=\\s*(true|false)\\s*(?:#.*)?$`, "m"));
117
+ return m ? m[1] === "true" : null;
118
+ }
119
+
120
+ /**
121
+ * TRUE when the codex `multi_agent_v2` feature is enabled in config.toml.
122
+ * Recognizes both shipped forms (codex-rs features/src/tests.rs):
123
+ * [features.multi_agent_v2] [features]
124
+ * enabled = true multi_agent_v2 = true
125
+ * plus the inline-table form `multi_agent_v2 = { enabled = true, ... }`.
126
+ * Missing file/key -> false (upstream default_enabled = false).
127
+ */
128
+ export function isMultiAgentV2Enabled(configPath?: string): boolean {
129
+ const content = readConfigText(configPath);
130
+ return multiAgentV2EnabledFromConfigText(content);
131
+ }
132
+
133
+ /** Parse `multi_agent_v2` from caller-owned config.toml text without consulting disk. */
134
+ export function multiAgentV2EnabledFromConfigText(content: string | null): boolean {
135
+ if (content === null) return false;
136
+
137
+ // Prefer a real parse. The hand-written table scanner below cannot distinguish
138
+ // an assignment from prose that looks like one — a `"""` value containing the
139
+ // line `enabled = true` reads as the key itself — and TOML has enough value
140
+ // shapes (multi-line arrays opening on the next line, escapes, comments) that
141
+ // each near-miss costs another special case (#1295).
142
+ //
143
+ // The scanner remains only for a document `Bun.TOML.parse` rejects. That is a
144
+ // statement about Bun's parser, not about Codex's — the two are separate
145
+ // implementations and no compatibility evidence is claimed here, so a document
146
+ // Bun rejects may still be one Codex loads. The fallback is therefore
147
+ // best-effort and inherits the ambiguity above. It exists because reporting a
148
+ // feature as disabled on account of an unreadable file presents a failure as
149
+ // a state.
150
+ const parsed = parsedTomlTable(content, "features");
151
+ if (parsed !== null) {
152
+ const table = plainTomlRecord(parsed.multi_agent_v2);
153
+ if (table !== null) return table.enabled === true;
154
+ if (typeof parsed.multi_agent_v2 === "boolean") return parsed.multi_agent_v2;
155
+ return false;
156
+ }
157
+
158
+ // Bun 1.4 enforces TOML's "value must begin on the assignment line" rule that
159
+ // 1.3.14 did not, so `hint =` followed by `[` on the next line now fails the
160
+ // real parse and reaches the line-based fallback below — which reads that `[`
161
+ // as a table header and truncates the table before `enabled`. Codex's own
162
+ // parser accepts the document, so answering "disabled" would report a parser
163
+ // disagreement as a feature state (#1295, #1691).
164
+ //
165
+ // Joining a dangling `=` to the line that follows is the smallest repair that
166
+ // keeps the scanner untouched: widening `tomlTableBody` to be string-aware is
167
+ // what previously broke `getAgentsEnabled`, `getAgentsMaxDepth`, and
168
+ // `getMaxConcurrentThreads` (see its comment). If the joined document parses,
169
+ // that answer is authoritative; if it does not, nothing is lost.
170
+ const joined = joinDanglingTomlAssignments(content);
171
+ if (joined !== content) {
172
+ const reparsed = parsedTomlTable(joined, "features");
173
+ if (reparsed !== null) {
174
+ const table = plainTomlRecord(reparsed.multi_agent_v2);
175
+ if (table !== null) return table.enabled === true;
176
+ if (typeof reparsed.multi_agent_v2 === "boolean") return reparsed.multi_agent_v2;
177
+ return false;
178
+ }
179
+ }
180
+
181
+ const table = tomlTableBody(content, "features.multi_agent_v2");
182
+ if (table !== null) {
183
+ const enabled = tomlBoolInBody(table, "enabled");
184
+ if (enabled !== null) return enabled;
185
+ // A bare [features.multi_agent_v2] table without `enabled` counts as on
186
+ // (FeatureToml::Config with enabled: None materializes as enabled upstream
187
+ // only when set; be conservative and require the boolean).
188
+ return false;
189
+ }
190
+
191
+ const features = tomlTableBody(content, "features");
192
+ if (features !== null) {
193
+ const bool = tomlBoolInBody(features, "multi_agent_v2");
194
+ if (bool !== null) return bool;
195
+ const inline = features.match(/^\s*multi_agent_v2\s*=\s*\{([^}]*)\}/m);
196
+ if (inline) {
197
+ const enabled = inline[1].match(/enabled\s*=\s*(true|false)/);
198
+ if (enabled) return enabled[1] === "true";
199
+ }
200
+ }
201
+ return false;
202
+ }
203
+
204
+ function plainTomlRecord(value: unknown): Record<string, unknown> | null {
205
+ return value !== null && typeof value === "object" && !Array.isArray(value)
206
+ ? value as Record<string, unknown>
207
+ : null;
208
+ }
209
+
210
+ /**
211
+ * Join `key =` to the following line when the value was written on the next
212
+ * line, so a parser enforcing TOML's same-line rule can read the document.
213
+ *
214
+ * Bun 1.3.14 accepted this shape; Bun 1.4 rejects it, correctly — TOML requires
215
+ * the value to begin on the assignment line. Codex's parser still accepts it, so
216
+ * this exists to keep the two readers agreeing rather than to endorse the shape.
217
+ *
218
+ * Deliberately narrow: it only acts on a line whose LAST non-comment character
219
+ * is `=`, which cannot occur in a valid assignment. Lines inside multi-line
220
+ * strings are left alone — a `"""` body line ending in `=` would be rewritten,
221
+ * but the result is only used when it PARSES, and the unmodified document is
222
+ * always tried first, so a wrong join cannot displace a correct read.
223
+ */
224
+ function joinDanglingTomlAssignments(content: string): string {
225
+ const lines = content.split("\n");
226
+ const out: string[] = [];
227
+ for (let i = 0; i < lines.length; i++) {
228
+ const line = lines[i]!;
229
+ // A dangling assignment: trailing `=` with nothing after it on this line.
230
+ if (/^[^#]*[^=!<>]=\s*$/.test(line) && i + 1 < lines.length) {
231
+ let j = i + 1;
232
+ // Skip blank and comment-only lines between the `=` and its value.
233
+ while (j < lines.length && /^\s*(?:#.*)?$/.test(lines[j]!)) j++;
234
+ if (j < lines.length) {
235
+ out.push(`${line.replace(/\s*$/, "")} ${lines[j]!.replace(/^\s*/, "")}`);
236
+ i = j;
237
+ continue;
238
+ }
239
+ }
240
+ out.push(line);
241
+ }
242
+ return out.join("\n");
243
+ }
244
+
245
+ /**
246
+ * A top-level table from a full TOML parse, or null when the document does not
247
+ * parse. A parsed document with no such table yields `{}` rather than null: that
248
+ * is a real answer ("no keys"), while null means "could not read, fall back".
249
+ */
250
+ function parsedTomlTable(content: string, name: string): Record<string, unknown> | null {
251
+ const toml = (globalThis as { Bun?: { TOML?: { parse(input: string): unknown } } }).Bun?.TOML;
252
+ if (!toml) return null;
253
+ try {
254
+ const root = plainTomlRecord(toml.parse(content));
255
+ if (root === null) return null;
256
+ return plainTomlRecord(root[name]) ?? {};
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+
262
+
263
+ /**
264
+ * TRUE when the codex `default_mode_request_user_input` feature is enabled in
265
+ * config.toml — lets a Default-mode session pause and ask the user questions
266
+ * through `request_user_input` (upstream FeatureSpec: under development,
267
+ * default_enabled = false). Recognizes the shipped boolean form
268
+ * `[features] default_mode_request_user_input = true`.
269
+ * Missing file/key -> false.
270
+ */
271
+ export function isDefaultModeRequestUserInputEnabled(configPath?: string): boolean {
272
+ const content = readConfigText(configPath);
273
+ if (content === null) return false;
274
+ // Same reason as the v2 reader: a `"""` value whose prose contains
275
+ // `default_mode_request_user_input = true` is not an assignment, and a raw
276
+ // regex over the table body cannot tell the difference (#1295).
277
+ const parsed = parsedTomlTable(content, "features");
278
+ if (parsed !== null) return parsed[DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY] === true;
279
+ const features = tomlTableBody(content, "features");
280
+ if (features === null) return false;
281
+ return tomlBoolInBody(features, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY) === true;
282
+ }
283
+
284
+ /**
285
+ * TRUE when config.toml still carries `[agents] max_threads` — codex-rs REFUSES to
286
+ * boot with that key while multi_agent_v2 is enabled ("agents.max_threads cannot be
287
+ * set when features.multi_agent_v2 is enabled", core/src/config/mod.rs:1421). The
288
+ * `ocx v2 on` flow warns about it instead of editing config itself.
289
+ */
290
+ export function hasAgentsMaxThreads(configPath?: string): boolean {
291
+ const content = readConfigText(configPath);
292
+ if (content === null) return false;
293
+ const parsed = parsedTomlTable(content, "agents");
294
+ if (parsed !== null) return Object.hasOwn(parsed, "max_threads");
295
+ const agents = tomlTableBody(content, "agents");
296
+ if (agents === null) return false;
297
+ return /^\s*max_threads\s*=/m.test(agents);
298
+ }
299
+
300
+ /** Current legacy v1 `[agents] max_threads`, or null when absent/invalid. */
301
+ export function getAgentsMaxThreads(configPath?: string): number | null {
302
+ const content = readConfigText(configPath);
303
+ if (content === null) return null;
304
+ const parsed = parsedTomlTable(content, "agents");
305
+ if (parsed !== null) {
306
+ const value = parsed.max_threads;
307
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null;
308
+ }
309
+ const agents = tomlTableBody(content, "agents");
310
+ if (agents === null) return null;
311
+ const m = agents.match(/^\s*max_threads\s*=\s*(\d+)\s*(?:#.*)?$/m);
312
+ if (!m) return null;
313
+ const value = Number(m[1]);
314
+ return Number.isInteger(value) && value >= 1 ? value : null;
315
+ }
316
+
317
+ /**
318
+ * Current `[agents] enabled`. Upstream defaults this to true and lets an enabled
319
+ * `features.multi_agent_v2` override it entirely (codex-rs core/src/config/mod.rs
320
+ * multi_agent_version_override returns V2 first at :1521-1523; `enabled = false`
321
+ * only takes effect with V2 off), so `null` means "unset, upstream default applies"
322
+ * and is NOT the same as `true`.
323
+ */
324
+ export function getAgentsEnabled(configPath?: string): boolean | null {
325
+ const content = readConfigText(configPath);
326
+ if (content === null) return null;
327
+ const agents = tomlTableBody(content, "agents");
328
+ if (agents === null) return null;
329
+ const m = agents.match(/^\s*enabled\s*=\s*(true|false)\s*(?:#.*)?$/m);
330
+ return m ? m[1] === "true" : null;
331
+ }
332
+
333
+ /**
334
+ * Current `[agents] max_depth`. Upstream applies this to V1 agent threads only and
335
+ * ignores it under V2 (config_toml.rs: "Maximum nesting depth for V1 agent threads.
336
+ * Ignored by V2."). The upstream type is `Option<i32>` with no minimum, so a
337
+ * negative value is valid config that effectively disables V1 child spawning —
338
+ * do not "correct" it, and do not present this as an effective V2 limit.
339
+ */
340
+ export function getAgentsMaxDepth(configPath?: string): number | null {
341
+ const content = readConfigText(configPath);
342
+ if (content === null) return null;
343
+ const agents = tomlTableBody(content, "agents");
344
+ if (agents === null) return null;
345
+ const m = agents.match(/^\s*max_depth\s*=\s*(-?\d+)\s*(?:#.*)?$/m);
346
+ if (!m) return null;
347
+ const value = Number(m[1]);
348
+ return Number.isInteger(value) && value >= -2_147_483_648 && value <= 2_147_483_647 ? value : null;
349
+ }
350
+
351
+ /**
352
+ * Current `features.multi_agent_v2.max_concurrent_threads_per_session`, from
353
+ * either the dedicated or inline-table form; null means the Codex default.
354
+ */
355
+ export function getMaxConcurrentThreads(configPath?: string): number | null {
356
+ const content = readConfigText(configPath);
357
+ if (content === null) return null;
358
+ const table = tomlTableBody(content, "features.multi_agent_v2");
359
+ const features = tomlTableBody(content, "features");
360
+ const inline = features?.match(/^\s*multi_agent_v2\s*=\s*\{([^}]*)\}/m);
361
+ const m = table?.match(/^\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?:#.*)?$/m)
362
+ ?? inline?.[1].match(/(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?:,|$)/);
363
+ if (!m) return null;
364
+ const value = Number(m[1]);
365
+ return Number.isFinite(value) && value >= 1 ? value : null;
366
+ }
367
+
368
+ /** Largest V1 child limit we translate. Well below Number.MAX_SAFE_INTEGER and far
369
+ * above any real concurrency setting; upstream's usize saturates, ours would silently
370
+ * lose precision. */
371
+ const MAX_TRANSLATABLE_V1_CHILD_LIMIT = 1_000_000;
372
+ /** The V2 side is one larger by construction: it counts the root agent's own slot, so
373
+ * the image of the maximum V1 value must itself be translatable back. */
374
+ const MAX_TRANSLATABLE_V2_TOTAL_LIMIT = MAX_TRANSLATABLE_V1_CHILD_LIMIT + 1;
375
+
376
+ export function isTranslatableV1ChildLimit(limit: number): boolean {
377
+ return Number.isInteger(limit) && limit >= 1 && limit <= MAX_TRANSLATABLE_V1_CHILD_LIMIT;
378
+ }
379
+
380
+ export function isTranslatableV2TotalLimit(limit: number): boolean {
381
+ return Number.isInteger(limit) && limit >= 1 && limit <= MAX_TRANSLATABLE_V2_TOTAL_LIMIT;
382
+ }
383
+
384
+ /**
385
+ * Upstream counts the root agent inside the V2 thread limit but not inside the legacy
386
+ * `[agents]` limit (codex-rs core/src/config/mod.rs resolve_multi_agent_v2_config applies
387
+ * saturating_add(1) to the [agents] value; the inverse saturating_sub(1) appears at
388
+ * mod.rs:1555). These helpers keep our migrations on the same side of that boundary.
389
+ */
390
+ export function v1ChildLimitToV2TotalLimit(childLimit: number): number {
391
+ if (!isTranslatableV1ChildLimit(childLimit)) {
392
+ throw new RangeError(`v1 child limit out of translatable range: ${childLimit}`);
393
+ }
394
+ return childLimit + 1;
395
+ }
396
+
397
+ /**
398
+ * Inverse of `v1ChildLimitToV2TotalLimit`. A V2 total of 1 means "root only, no
399
+ * children", which has no representable legacy child count >= 1, so it clamps to 1
400
+ * rather than writing 0 and tripping upstream's `>= 1` validation.
401
+ */
402
+ export function v2TotalLimitToV1ChildLimit(totalLimit: number): number {
403
+ if (!isTranslatableV2TotalLimit(totalLimit)) {
404
+ throw new RangeError(`v2 total limit out of translatable range: ${totalLimit}`);
405
+ }
406
+ return Math.max(1, totalLimit - 1);
407
+ }
408
+
409
+ /**
410
+ * Persist `features.multi_agent_v2.max_concurrent_threads_per_session = value`.
411
+ * Scoped edit in either the dedicated table or `[features]` boolean/inline form.
412
+ * Boolean form is upgraded to an inline config so the numeric value remains
413
+ * attached to the feature without a TOML key conflict. Idempotent on equal value.
414
+ */
415
+ export function setMaxConcurrentThreads(value: number, configPath?: string, migratedComment?: string): { ok: true; changed: boolean } | { ok: false; error: string } {
416
+ if (!Number.isInteger(value) || value < 1) {
417
+ return { ok: false, error: "max_concurrent_threads_per_session must be an integer >= 1" };
418
+ }
419
+ const path = configPath ?? activeCodexConfigPath();
420
+ const content = readConfigText(path);
421
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
422
+
423
+ const eol = dominantEol(content);
424
+ const lines = content.split(/\r?\n/);
425
+ const headerRe = /^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/;
426
+ const headerIdx = lines.findIndex(l => headerRe.test(l));
427
+ if (headerIdx === -1) {
428
+ const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
429
+ if (featuresHeader === -1) return { ok: false, error: "multi_agent_v2 feature config not found — enable v2 first (ocx v2 on)" };
430
+ let featuresEnd = lines.length;
431
+ for (let i = featuresHeader + 1; i < lines.length; i++) {
432
+ if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
433
+ }
434
+ const boolRe = /^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*#.*)?$/;
435
+ const inlineRe = /^(\s*)multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)?$/;
436
+ for (let i = featuresHeader + 1; i < featuresEnd; i++) {
437
+ const bool = lines[i].match(boolRe);
438
+ if (bool) {
439
+ lines[i] = `${bool[1]}multi_agent_v2 = { enabled = ${bool[2]}, max_concurrent_threads_per_session = ${value} }${mergeTrailingComments(bool[3], migratedComment)}`;
440
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
441
+ return { ok: true, changed: true };
442
+ }
443
+ const inline = lines[i].match(inlineRe);
444
+ if (!inline) continue;
445
+ const existing = inline[2].match(/(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?=,|$)/);
446
+ if (existing && Number(existing[1]) === value && (!migratedComment || migratedComment === inline[3])) return { ok: true, changed: false };
447
+ const body = existing
448
+ ? inline[2].replace(/(^|,)\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, `$1 max_concurrent_threads_per_session = ${value}`)
449
+ : `${inline[2].trim()}${inline[2].trim() ? ", " : ""}max_concurrent_threads_per_session = ${value}`;
450
+ lines[i] = `${inline[1]}multi_agent_v2 = { ${body.trim()} }${mergeTrailingComments(inline[3], migratedComment)}`;
451
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
452
+ return { ok: true, changed: true };
453
+ }
454
+ return { ok: false, error: "multi_agent_v2 feature config not found — enable v2 first (ocx v2 on)" };
455
+ }
456
+ let end = lines.length;
457
+ for (let i = headerIdx + 1; i < lines.length; i++) {
458
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
459
+ }
460
+ const keyRe = /^(\s*)max_concurrent_threads_per_session\s*=\s*(\d+)(\s*#.*)?$/;
461
+ for (let i = headerIdx + 1; i < end; i++) {
462
+ const m = lines[i].match(keyRe);
463
+ if (!m) continue;
464
+ if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { ok: true, changed: false };
465
+ lines[i] = `${m[1]}max_concurrent_threads_per_session = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
466
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
467
+ return { ok: true, changed: true };
468
+ }
469
+ lines.splice(headerIdx + 1, 0, `max_concurrent_threads_per_session = ${value}${migratedComment ?? ""}`);
470
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
471
+ return { ok: true, changed: true };
472
+ }
473
+
474
+ type ConfigEditResult = { ok: true; changed: boolean } | { ok: false; error: string };
475
+
476
+ /**
477
+ * Encode a string as a TOML single-line basic string.
478
+ *
479
+ * Character-by-character on purpose. A chained-replace implementation
480
+ * (`.replace(/\\/g, "\\\\").replace(/\t/g, "\\t")`) corrupts input: the backslash
481
+ * pass runs first, then later passes insert NEW backslashes the first pass can no
482
+ * longer protect. Single-line basic strings handle every case including embedded
483
+ * `"""`, which a multi-line `"""..."""` form cannot.
484
+ */
485
+ function encodeTomlBasicString(value: string): string {
486
+ let out = '"';
487
+ for (const ch of value) {
488
+ switch (ch) {
489
+ case "\\": out += "\\\\"; break;
490
+ case '"': out += '\\"'; break;
491
+ case "\n": out += "\\n"; break;
492
+ case "\r": out += "\\r"; break;
493
+ case "\t": out += "\\t"; break;
494
+ case "\b": out += "\\b"; break;
495
+ case "\f": out += "\\f"; break;
496
+ default: {
497
+ const code = ch.codePointAt(0)!;
498
+ out += code < 0x20 || code === 0x7f
499
+ ? `\\u${code.toString(16).padStart(4, "0")}`
500
+ : ch;
501
+ }
502
+ }
503
+ }
504
+ return out + '"';
505
+ }
506
+
507
+ /**
508
+ * Decode one TOML string token INCLUDING its quotes. Basic strings (`"..."`)
509
+ * unescape; literal strings (`'...'`) are verbatim — a backslash is not special
510
+ * there. Returns null for anything that is not a string token.
511
+ */
512
+ function decodeTomlStringToken(token: string): string | null {
513
+ if (token.length < 2) return null;
514
+ // Multi-line literal string: `'''...'''` verbatim (backslashes not special).
515
+ if (token.startsWith("'''")) {
516
+ return token.endsWith("'''") && token.length >= 6
517
+ ? normalizeTomlMultilineBody(token.slice(3, -3))
518
+ : null;
519
+ }
520
+ // Multi-line basic string: `"""..."""` with escapes.
521
+ if (token.startsWith('"""')) {
522
+ if (!token.endsWith('"""') || token.length < 6) return null;
523
+ return decodeBasicStringBody(normalizeTomlMultilineBody(token.slice(3, -3)), true);
524
+ }
525
+ if (token.startsWith("'")) {
526
+ return token.endsWith("'") ? token.slice(1, -1) : null;
527
+ }
528
+ if (!token.startsWith('"') || !token.endsWith('"')) return null;
529
+ return decodeBasicStringBody(token.slice(1, -1));
530
+ }
531
+
532
+ /** Apply TOML's multi-line newline rules before string-body decoding. */
533
+ function normalizeTomlMultilineBody(body: string): string {
534
+ const normalized = body.replace(/\r\n/g, "\n");
535
+ return normalized.startsWith("\n") ? normalized.slice(1) : normalized;
536
+ }
537
+
538
+ /** Unescape the body of a TOML basic string (single- or multi-line). */
539
+ function decodeBasicStringBody(body: string, multiline = false): string | null {
540
+ let out = "";
541
+ for (let i = 0; i < body.length; i++) {
542
+ const ch = body[i];
543
+ if (ch !== "\\") { out += ch; continue; }
544
+ const esc = body[++i];
545
+ if (multiline) {
546
+ let newline = i;
547
+ while (body[newline] === " " || body[newline] === "\t") newline++;
548
+ if (body[newline] === "\n") {
549
+ i = newline;
550
+ while (body[i + 1] === " " || body[i + 1] === "\t" || body[i + 1] === "\n") i++;
551
+ continue;
552
+ }
553
+ }
554
+ switch (esc) {
555
+ case "\\": out += "\\"; break;
556
+ case '"': out += '"'; break;
557
+ case "n": out += "\n"; break;
558
+ case "r": out += "\r"; break;
559
+ case "t": out += "\t"; break;
560
+ case "b": out += "\b"; break;
561
+ case "f": out += "\f"; break;
562
+ case "u": {
563
+ const code = parseInt(body.slice(i + 1, i + 5), 16);
564
+ if (Number.isNaN(code)) return null;
565
+ out += String.fromCodePoint(code);
566
+ i += 4;
567
+ break;
568
+ }
569
+ case "U": {
570
+ const code = parseInt(body.slice(i + 1, i + 9), 16);
571
+ if (Number.isNaN(code)) return null;
572
+ out += String.fromCodePoint(code);
573
+ i += 8;
574
+ break;
575
+ }
576
+ default: return null;
577
+ }
578
+ }
579
+ return out;
580
+ }
581
+
582
+ /**
583
+ * End index (exclusive) of the TOML value starting at or after `start` in `text`.
584
+ * String-aware: basic strings honor backslash escapes, literal strings do not.
585
+ * Inline tables and arrays nest and are scanned with the same awareness.
586
+ */
587
+ function scanTomlValueEnd(text: string, start: number): number {
588
+ let i = start;
589
+ while (i < text.length && (text[i] === " " || text[i] === "\t")) i++;
590
+ const first = text[i];
591
+ if (first === '"') {
592
+ // Multi-line basic string: `"""..."""`. The closing delimiter is a triple
593
+ // quote; a single or double quote inside the body does not end the token.
594
+ if (text[i + 1] === '"' && text[i + 2] === '"') {
595
+ i += 3;
596
+ while (i < text.length) {
597
+ if (text[i] === "\\") { i += 2; continue; }
598
+ if (text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
599
+ // TOML permits up to two quotes immediately inside the closing
600
+ // delimiter, so a 4- or 5-quote run means the extra one or two
601
+ // quotes are part of the value. Consume the full valid run and
602
+ // keep the surplus quotes in the body.
603
+ let end = i + 3;
604
+ if (text[end] === '"') {
605
+ end++;
606
+ if (text[end] === '"') end++;
607
+ }
608
+ return end;
609
+ }
610
+ i++;
611
+ }
612
+ return text.length;
613
+ }
614
+ i++;
615
+ while (i < text.length) {
616
+ if (text[i] === "\\") { i += 2; continue; }
617
+ if (text[i] === '"') return i + 1;
618
+ i++;
619
+ }
620
+ return text.length;
621
+ }
622
+ if (first === "'") {
623
+ // Multi-line literal string: `'''...'''`.
624
+ if (text[i + 1] === "'" && text[i + 2] === "'") {
625
+ i += 3;
626
+ while (i < text.length) {
627
+ if (text[i] === "'" && text[i + 1] === "'" && text[i + 2] === "'") {
628
+ // Same as multi-line basic: up to two surplus single quotes can
629
+ // precede the closing delimiter and remain part of the value.
630
+ let end = i + 3;
631
+ if (text[end] === "'") {
632
+ end++;
633
+ if (text[end] === "'") end++;
634
+ }
635
+ return end;
636
+ }
637
+ i++;
638
+ }
639
+ return text.length;
640
+ }
641
+ const close = text.indexOf("'", i + 1);
642
+ return close === -1 ? text.length : close + 1;
643
+ }
644
+ if (first === "{") {
645
+ const close = findInlineTableEnd(text, i);
646
+ return close === -1 ? text.length : close + 1;
647
+ }
648
+ if (first === "[") {
649
+ let depth = 0;
650
+ while (i < text.length) {
651
+ const c = text[i];
652
+ if (c === '"' || c === "'") { i = scanTomlValueEnd(text, i); continue; }
653
+ if (c === "[") depth++;
654
+ else if (c === "]") { depth--; if (depth === 0) return i + 1; }
655
+ i++;
656
+ }
657
+ return text.length;
658
+ }
659
+ while (i < text.length && !/[\s,}\]#]/.test(text[i])) i++;
660
+ return i;
661
+ }
662
+
663
+ /** Index of the `}` matching the `{` at `openIdx`, string-aware, or -1. */
664
+ function findInlineTableEnd(text: string, openIdx: number): number {
665
+ let depth = 0;
666
+ let i = openIdx;
667
+ while (i < text.length) {
668
+ const c = text[i];
669
+ if (c === '"' || c === "'") { i = scanTomlValueEnd(text, i); continue; }
670
+ if (c === "{") depth++;
671
+ else if (c === "}") { depth--; if (depth === 0) return i; }
672
+ i++;
673
+ }
674
+ return -1;
675
+ }
676
+
677
+ interface InlineEntry { keyStart: number; valueStart: number; valueEnd: number }
678
+
679
+ /** Locate a top-level assignment while skipping complete (possibly multiline) values. */
680
+ function findTomlAssignment(text: string, key: string): InlineEntry | null {
681
+ let lineStart = 0;
682
+ while (lineStart < text.length) {
683
+ let i = lineStart;
684
+ while (text[i] === " " || text[i] === "\t") i++;
685
+ const keyStart = i;
686
+ let keyText = "";
687
+ if (text[i] === '"' || text[i] === "'") {
688
+ const keyEnd = scanTomlValueEnd(text, i);
689
+ keyText = decodeTomlStringToken(text.slice(i, keyEnd)) ?? "";
690
+ i = keyEnd;
691
+ } else {
692
+ const match = /^[A-Za-z0-9_-]+/.exec(text.slice(i));
693
+ if (match) {
694
+ keyText = match[0];
695
+ i += match[0].length;
696
+ }
697
+ }
698
+ while (text[i] === " " || text[i] === "\t") i++;
699
+ if (keyText && text[i] === "=") {
700
+ const valueStart = i + 1;
701
+ const valueEnd = scanTomlValueEnd(text, valueStart);
702
+ if (keyText === key) return { keyStart, valueStart, valueEnd };
703
+ const nextLine = text.indexOf("\n", valueEnd);
704
+ lineStart = nextLine === -1 ? text.length : nextLine + 1;
705
+ continue;
706
+ }
707
+ const nextLine = text.indexOf("\n", lineStart);
708
+ lineStart = nextLine === -1 ? text.length : nextLine + 1;
709
+ }
710
+ return null;
711
+ }
712
+
713
+ /**
714
+ * Locate `key = value` inside the inline-table body spanning [bodyStart, bodyEnd)
715
+ * (exclusive of the braces), string-aware on both keys and values, or null.
716
+ */
717
+ function findInlineEntry(text: string, bodyStart: number, bodyEnd: number, key: string): InlineEntry | null {
718
+ let i = bodyStart;
719
+ while (i < bodyEnd) {
720
+ while (i < bodyEnd && /[\s,]/.test(text[i])) i++;
721
+ if (i >= bodyEnd) break;
722
+ const entryStart = i;
723
+ let keyText: string;
724
+ if (text[i] === '"' || text[i] === "'") {
725
+ const keyEnd = scanTomlValueEnd(text, i);
726
+ keyText = decodeTomlStringToken(text.slice(i, keyEnd)) ?? "";
727
+ i = keyEnd;
728
+ } else {
729
+ const m = /^[A-Za-z0-9_-]+/.exec(text.slice(i, bodyEnd));
730
+ if (!m) break;
731
+ keyText = m[0];
732
+ i += m[0].length;
733
+ }
734
+ while (i < bodyEnd && /\s/.test(text[i])) i++;
735
+ if (text[i] !== "=") { i = entryStart + 1; continue; }
736
+ i++;
737
+ while (i < bodyEnd && /\s/.test(text[i])) i++;
738
+ const valueStart = i;
739
+ const valueEnd = Math.min(scanTomlValueEnd(text, valueStart), bodyEnd);
740
+ if (keyText === key) return { keyStart: entryStart, valueStart, valueEnd };
741
+ i = valueEnd;
742
+ }
743
+ return null;
744
+ }
745
+
746
+ /** Whether a TOML assignment's value starts with a triple-quoted string. */
747
+ function isMultilineTomlString(text: string, valueStart: number): boolean {
748
+ let start = valueStart;
749
+ while (text[start] === " " || text[start] === "\t") start++;
750
+ return text.startsWith('"""', start) || text.startsWith("'''", start);
751
+ }
752
+
753
+ /** Whether one field in a supported inline `multi_agent_v2` table is triple-quoted. */
754
+ function hasInlineMultilineTomlString(text: string, assignment: InlineEntry, key: string): boolean {
755
+ let openIdx = assignment.valueStart;
756
+ while (text[openIdx] === " " || text[openIdx] === "\t") openIdx++;
757
+ if (text[openIdx] !== "{") return false;
758
+ const closeIdx = findInlineTableEnd(text, openIdx);
759
+ if (closeIdx === -1) return false;
760
+ const entry = findInlineEntry(text, openIdx + 1, closeIdx, key);
761
+ return entry !== null && isMultilineTomlString(text, entry.valueStart);
762
+ }
763
+
764
+ /**
765
+ * Set or remove one scalar key inside a top-level TOML table, preserving every
766
+ * other line byte-for-byte — including the existing value's trailing comment,
767
+ * which is kept verbatim. `encoded` is the already-serialized RHS
768
+ * (`encodeTomlBasicString` for strings, `String(n)` for numbers, `"true"`/`"false"`
769
+ * for booleans); null removes the key. Creates the table when absent. Returns the
770
+ * new content; returning the input unchanged means no-op.
771
+ */
772
+ function editScalarInTable(content: string, table: string, key: string, encoded: string | null): string {
773
+ const eol = dominantEol(content);
774
+ const lines = content.split(/\r?\n/);
775
+ const headerRe = new RegExp(`^\\s*\\[${table.replace(/\./g, "\\.")}\\]\\s*(?:#.*)?$`);
776
+ const headerIdx = lines.findIndex(l => headerRe.test(l));
777
+ if (headerIdx === -1) {
778
+ if (encoded === null) return content;
779
+ const separator = lines.length > 0 && lines[lines.length - 1] !== "" ? [""] : [];
780
+ lines.push(...separator, `[${table}]`, `${key} = ${encoded}`);
781
+ return applyEol(lines.join("\n"), eol);
782
+ }
783
+ let end = lines.length;
784
+ for (let i = headerIdx + 1; i < lines.length; i++) {
785
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
786
+ }
787
+ for (let i = headerIdx + 1; i < end; i++) {
788
+ const entry = findTomlAssignment(lines[i], key);
789
+ if (!entry) continue;
790
+ // The existing value may itself contain '#', so scan the value token
791
+ // string-aware instead of splitting on '#'.
792
+ const line = lines[i];
793
+ const trailing = line.slice(entry.valueEnd);
794
+ if (encoded === null) {
795
+ lines.splice(i, 1);
796
+ return applyEol(lines.join("\n"), eol);
797
+ }
798
+ if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return content;
799
+ lines[i] = `${line.slice(0, entry.keyStart)}${key} = ${encoded}${trailing}`;
800
+ return applyEol(lines.join("\n"), eol);
801
+ }
802
+ if (encoded === null) return content;
803
+ lines.splice(headerIdx + 1, 0, `${key} = ${encoded}`);
804
+ return applyEol(lines.join("\n"), eol);
805
+ }
806
+
807
+ /** Persist `[agents] enabled = value`, or remove the key when `value` is null. */
808
+ export function setAgentsEnabled(value: boolean | null, configPath?: string): ConfigEditResult {
809
+ const path = configPath ?? activeCodexConfigPath();
810
+ const content = readConfigText(path);
811
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
812
+ const next = editScalarInTable(content, "agents", "enabled", value === null ? null : String(value));
813
+ if (next === content) return { ok: true, changed: false };
814
+ atomicWriteFile(path, next);
815
+ return { ok: true, changed: true };
816
+ }
817
+
818
+ /**
819
+ * Persist `[agents] max_depth = value`, or remove the key when `value` is null.
820
+ * Validation is exactly the upstream contract: `Option<i32>` with no minimum, so
821
+ * any integer in signed-i32 range is accepted — writing anything wider would
822
+ * produce a config upstream cannot deserialize, a hard parse failure for the
823
+ * user's Codex.
824
+ */
825
+ export function setAgentsMaxDepth(value: number | null, configPath?: string): ConfigEditResult {
826
+ if (value !== null && (!Number.isInteger(value) || value < -2_147_483_648 || value > 2_147_483_647)) {
827
+ return { ok: false, error: "max_depth must be an integer within signed i32 range" };
828
+ }
829
+ const path = configPath ?? activeCodexConfigPath();
830
+ const content = readConfigText(path);
831
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
832
+ const next = editScalarInTable(content, "agents", "max_depth", value === null ? null : String(value));
833
+ if (next === content) return { ok: true, changed: false };
834
+ atomicWriteFile(path, next);
835
+ return { ok: true, changed: true };
836
+ }
837
+
838
+ /**
839
+ * Current `features.multi_agent_v2.subagent_developer_instructions`.
840
+ *
841
+ * Upstream tri-state (codex-rs core/src/config/mod.rs resolve_multi_agent_v2_config):
842
+ * unset -> the child inherits the parent's developer instructions
843
+ * non-empty -> replaces the inherited parent fragment
844
+ * empty string -> clears the inherited fragment
845
+ *
846
+ * So `null` and `""` are DIFFERENT values and both must round-trip. Upstream
847
+ * `.trim()`s the configured text, so whitespace-only values are effectively `""`.
848
+ * Reads both the dedicated-table and inline forms; dedicated wins, mirroring
849
+ * `getMaxConcurrentThreads` precedence.
850
+ */
851
+ /**
852
+ * Read a string-valued `features.multi_agent_v2` scalar (dedicated table or inline
853
+ * form; dedicated wins, mirroring `getMaxConcurrentThreads` precedence). Returns
854
+ * `null` when the key is absent or the config is unreadable. A present empty string
855
+ * round-trips faithfully — some upstream keys treat `""` and `null` differently.
856
+ */
857
+ function getV2StringField(key: string, configPath?: string): string | null {
858
+ const content = readConfigText(configPath);
859
+ if (content === null) return null;
860
+ const table = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
861
+ if (table !== null) {
862
+ const entry = findTomlAssignment(table, key);
863
+ if (entry) {
864
+ const token = table.slice(entry.valueStart, entry.valueEnd).trim();
865
+ return decodeTomlStringToken(token);
866
+ }
867
+ return null;
868
+ }
869
+ const features = tomlTableBodyForStringFields(content, "features");
870
+ if (features === null) return null;
871
+ const v2Entry = findTomlAssignment(features, "multi_agent_v2");
872
+ if (!v2Entry) return null;
873
+ let openIdx = v2Entry.valueStart;
874
+ while (features[openIdx] === " " || features[openIdx] === "\t") openIdx++;
875
+ if (features[openIdx] !== "{") return null;
876
+ const closeIdx = findInlineTableEnd(features, openIdx);
877
+ if (closeIdx === -1) return null;
878
+ const entry = findInlineEntry(features, openIdx + 1, closeIdx, key);
879
+ if (!entry) return null;
880
+ return decodeTomlStringToken(features.slice(entry.valueStart, entry.valueEnd).trim());
881
+ }
882
+
883
+ /**
884
+ * A table body scanner that skips complete TOML values before recognizing the
885
+ * next header. Unlike the legacy line scanner, bracket-shaped prose inside a
886
+ * multi-line string cannot truncate the table.
887
+ */
888
+ function tomlTableBodyForStringFields(content: string, header: string): string | null {
889
+ const escaped = escapeRegExp(header);
890
+ const match = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`, "m").exec(content);
891
+ if (!match) return null;
892
+ const newline = content.indexOf("\n", match.index + match[0].length);
893
+ if (newline === -1) return "";
894
+ const bodyStart = newline + 1;
895
+ let lineStart = bodyStart;
896
+ while (lineStart < content.length) {
897
+ let cursor = lineStart;
898
+ while (content[cursor] === " " || content[cursor] === "\t") cursor++;
899
+ if (content[cursor] === "[") return content.slice(bodyStart, lineStart);
900
+ const lineEnd = content.indexOf("\n", cursor);
901
+ const boundedEnd = lineEnd === -1 ? content.length : lineEnd;
902
+ let keyEnd = cursor;
903
+ if (content[keyEnd] === '"' || content[keyEnd] === "'") {
904
+ keyEnd = scanTomlValueEnd(content, keyEnd);
905
+ } else {
906
+ const keyMatch = /^[A-Za-z0-9_.-]+/.exec(content.slice(keyEnd, boundedEnd));
907
+ if (keyMatch) keyEnd += keyMatch[0].length;
908
+ }
909
+ while (content[keyEnd] === " " || content[keyEnd] === "\t") keyEnd++;
910
+ if (content[keyEnd] === "=" && keyEnd < boundedEnd) {
911
+ const valueEnd = scanTomlValueEnd(content, keyEnd + 1);
912
+ const nextLine = content.indexOf("\n", valueEnd);
913
+ lineStart = nextLine === -1 ? content.length : nextLine + 1;
914
+ } else {
915
+ lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
916
+ }
917
+ }
918
+ return content.slice(bodyStart);
919
+ }
920
+
921
+ function escapeRegExp(value: string): string {
922
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
923
+ }
924
+
925
+ /** Bare and quoted TOML forms of one known-safe key. */
926
+ function tomlKeyPattern(key: string): string {
927
+ const escaped = escapeRegExp(key);
928
+ return `(?:${escaped}|"${escaped}"|'${escaped}')`;
929
+ }
930
+
931
+ export function getSubagentDeveloperInstructions(configPath?: string): string | null {
932
+ return getV2StringField("subagent_developer_instructions", configPath);
933
+ }
934
+
935
+ /** Current `features.multi_agent_v2.multi_agent_mode_hint_text`, or null when unset. */
936
+ export function getMultiAgentModeHintText(configPath?: string): string | null {
937
+ return getV2StringField("multi_agent_mode_hint_text", configPath);
938
+ }
939
+
940
+ /**
941
+ * Persist `features.multi_agent_v2.subagent_developer_instructions`, or remove the
942
+ * key when `value` is null. Handles all three existing encodings: the dedicated
943
+ * table (scalar edit), the inline table (string-aware edit inside the braces — a
944
+ * regex over `[^}]*` would corrupt any value containing `}`), and the bare boolean
945
+ * form (upgraded in place to an inline table, mirroring `setMaxConcurrentThreads`).
946
+ * With no existing v2 config, creates the dedicated table carrying only this key.
947
+ * The key name must match upstream character-for-character: the upstream struct
948
+ * carries `#[serde(deny_unknown_fields)]`, so a misspelling is not ignored — it is
949
+ * a hard config-parse failure for the user's Codex.
950
+ */
951
+ function setV2StringField(key: string, value: string | null, configPath?: string): ConfigEditResult {
952
+ const path = configPath ?? activeCodexConfigPath();
953
+ const content = readConfigText(path);
954
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
955
+ const encoded = value === null ? null : encodeTomlBasicString(value);
956
+
957
+ // A parsed V2 object without one of the supported source forms came from
958
+ // dotted/quoted path segments. Appending a dedicated table would redefine it
959
+ // and make Codex reject the file, so fail closed without changing any bytes.
960
+ const parsedFeatures = parsedTomlTable(content, "features");
961
+ const parsedV2 = parsedFeatures === null ? null : plainTomlRecord(parsedFeatures.multi_agent_v2);
962
+ const dedicatedV2 = tomlTableBodyForStringFields(content, "features.multi_agent_v2") !== null;
963
+ const featuresBody = tomlTableBodyForStringFields(content, "features");
964
+ const featuresV2Entry = featuresBody === null ? null : findTomlAssignment(featuresBody, "multi_agent_v2");
965
+ const supportedFeaturesEntry = featuresV2Entry !== null;
966
+ if (parsedV2 !== null && !dedicatedV2 && !supportedFeaturesEntry) {
967
+ return { ok: false, error: "dotted or quoted multi_agent_v2 config is not supported for managed string fields" };
968
+ }
969
+
970
+ const dedicatedStringBody = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
971
+ if (dedicatedStringBody !== null) {
972
+ // Multiline TOML strings ("""...""" / '''...''') span multiple lines and the
973
+ // single-line table editor cannot rewrite or remove them without corrupting
974
+ // the document. Scope this guard to the target V2 table so an unrelated
975
+ // table carrying the same key does not block an otherwise safe edit.
976
+ const dedicatedEntry = findTomlAssignment(dedicatedStringBody, key);
977
+ if (dedicatedEntry !== null && isMultilineTomlString(dedicatedStringBody, dedicatedEntry.valueStart)) {
978
+ return { ok: false, error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
979
+ }
980
+ const legacyDedicatedBody = tomlTableBody(content, "features.multi_agent_v2") ?? "";
981
+ if (dedicatedEntry !== null && findTomlAssignment(legacyDedicatedBody, key) === null) {
982
+ return { ok: false, error: `cannot edit ${key} after a header-shaped multiline value safely` };
983
+ }
984
+ const next = editScalarInTable(content, "features.multi_agent_v2", key, encoded);
985
+ if (next === content) return { ok: true, changed: false };
986
+ atomicWriteFile(path, next);
987
+ return { ok: true, changed: true };
988
+ }
989
+
990
+ if (featuresBody !== null && featuresV2Entry !== null && hasInlineMultilineTomlString(featuresBody, featuresV2Entry, key)) {
991
+ return { ok: false, error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
992
+ }
993
+
994
+ const eol = dominantEol(content);
995
+ const lines = content.split(/\r?\n/);
996
+ const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
997
+ if (featuresHeader !== -1) {
998
+ let featuresEnd = lines.length;
999
+ for (let i = featuresHeader + 1; i < lines.length; i++) {
1000
+ if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
1001
+ }
1002
+ for (let i = featuresHeader + 1; i < featuresEnd; i++) {
1003
+ const line = lines[i];
1004
+ const inlineMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*\{/);
1005
+ if (inlineMatch) {
1006
+ const openIdx = inlineMatch[0].length - 1;
1007
+ const closeIdx = findInlineTableEnd(line, openIdx);
1008
+ if (closeIdx === -1) return { ok: false, error: "malformed multi_agent_v2 inline table" };
1009
+ const entry = findInlineEntry(line, openIdx + 1, closeIdx, key);
1010
+ if (encoded === null) {
1011
+ if (!entry) return { ok: true, changed: false };
1012
+ let start = entry.keyStart;
1013
+ let stop = entry.valueEnd;
1014
+ let j = stop;
1015
+ while (j < closeIdx && line[j] === " ") j++;
1016
+ if (line[j] === ",") {
1017
+ stop = j + 1;
1018
+ while (stop < closeIdx && line[stop] === " ") stop++;
1019
+ } else {
1020
+ let k = start;
1021
+ while (k > openIdx + 1 && line[k - 1] === " ") k--;
1022
+ if (line[k - 1] === ",") start = k - 1;
1023
+ }
1024
+ lines[i] = line.slice(0, start) + line.slice(stop);
1025
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1026
+ return { ok: true, changed: true };
1027
+ }
1028
+ if (entry) {
1029
+ if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return { ok: true, changed: false };
1030
+ lines[i] = line.slice(0, entry.valueStart) + encoded + line.slice(entry.valueEnd);
1031
+ } else {
1032
+ let insertPos = closeIdx;
1033
+ while (insertPos > openIdx + 1 && line[insertPos - 1] === " ") insertPos--;
1034
+ const hasEntries = line.slice(openIdx + 1, insertPos).trim().length > 0;
1035
+ const insertion = hasEntries
1036
+ ? `, ${key} = ${encoded} `
1037
+ : ` ${key} = ${encoded} `;
1038
+ lines[i] = line.slice(0, insertPos) + insertion + line.slice(closeIdx);
1039
+ }
1040
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1041
+ return { ok: true, changed: true };
1042
+ }
1043
+ const boolMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*(?:#.*)?)$/);
1044
+ if (boolMatch) {
1045
+ if (encoded === null) return { ok: true, changed: false };
1046
+ lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]}, ${key} = ${encoded} }${boolMatch[3]}`;
1047
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1048
+ return { ok: true, changed: true };
1049
+ }
1050
+ }
1051
+ if (featuresV2Entry !== null) {
1052
+ return { ok: false, error: "multi_agent_v2 inside a multiline [features] table is not editable safely" };
1053
+ }
1054
+ }
1055
+
1056
+ if (encoded === null) return { ok: true, changed: false };
1057
+ const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
1058
+ const separator = content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : "";
1059
+ const tableText = `[features.multi_agent_v2]${eol}${key} = ${encoded}${eol}`;
1060
+ atomicWriteFile(path, `${content}${suffix}${separator}${tableText}`);
1061
+ return { ok: true, changed: true };
1062
+ }
1063
+
1064
+ export function setSubagentDeveloperInstructions(value: string | null, configPath?: string): ConfigEditResult {
1065
+ return setV2StringField("subagent_developer_instructions", value, configPath);
1066
+ }
1067
+
1068
+ export const MODE_HINT_UNSUPPORTED_ERROR =
1069
+ "installed Codex does not support multi_agent_mode_hint_text; update Codex first";
1070
+
1071
+ /**
1072
+ * Persist `features.multi_agent_v2.multi_agent_mode_hint_text`, or remove the key
1073
+ * when `value` is null. Same encoding coverage and upstream-name discipline as
1074
+ * `setSubagentDeveloperInstructions`; the upstream struct rejects unknown fields,
1075
+ * so the key spelling must match codex-rs exactly.
1076
+ */
1077
+ export function setMultiAgentModeHintText(value: string | null, configPath?: string): ConfigEditResult {
1078
+ // The upstream `multi_agent_mode_hint_text` key is newer than the v2 config
1079
+ // surface opencodex already manages; an older Codex build rejects the unknown
1080
+ // member (`#[serde(deny_unknown_fields)]`) and fails to start. Probe the
1081
+ // installed runtime binary for the key string and refuse the write when the
1082
+ // binary provably lacks it. A probe that cannot run (missing binary,
1083
+ // unreadable file) does not block: that is the test/hermetic path and the
1084
+ // headless runtime fallback.
1085
+ if (value !== null) {
1086
+ const probe = probeCodexSupportsModeHint();
1087
+ if (probe === false) {
1088
+ return {
1089
+ ok: false,
1090
+ error: MODE_HINT_UNSUPPORTED_ERROR,
1091
+ };
1092
+ }
1093
+ }
1094
+ return setV2StringField("multi_agent_mode_hint_text", value, configPath);
1095
+ }
1096
+
1097
+ export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8;
1098
+ export const modeHintCapabilityCache = new Map<string, boolean | null>();
1099
+
1100
+ export function rememberModeHintCapability(cacheKey: string, capability: boolean | null): void {
1101
+ modeHintCapabilityCache.delete(cacheKey);
1102
+ modeHintCapabilityCache.set(cacheKey, capability);
1103
+ while (modeHintCapabilityCache.size > MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES) {
1104
+ const oldest = modeHintCapabilityCache.keys().next().value;
1105
+ if (oldest === undefined) break;
1106
+ modeHintCapabilityCache.delete(oldest);
1107
+ }
1108
+ }
1109
+
1110
+ /**
1111
+ * True when the installed Codex runtime binary contains the
1112
+ * `multi_agent_mode_hint_text` config key, false when it provably does not, and
1113
+ * null when the probe could not run (missing binary, unreadable file).
1114
+ */
1115
+ export function probeCodexSupportsModeHint(): boolean | null {
1116
+ try {
1117
+ const runtime = resolveAndPersistCodexRuntime({ env: process.env }).runtime;
1118
+ const selectedPath = resolveSelectedCommandPath(runtime.command);
1119
+ let selectedIdentity = selectedPath ?? "";
1120
+ try { if (selectedPath) selectedIdentity = realpathSync(selectedPath); } catch { /* keep lexical path */ }
1121
+ const candidates = codexNativeBinaryCandidates(runtime.command);
1122
+ const binaryStatFingerprint = candidates.map(candidate => {
1123
+ try {
1124
+ const stat = statSync(candidate);
1125
+ return `${candidate}\0${stat.dev}\0${stat.ino}\0${stat.size}\0${stat.mtimeMs}\0${stat.ctimeMs}`;
1126
+ } catch {
1127
+ return `${candidate}\0missing`;
1128
+ }
1129
+ }).join("\0");
1130
+ const cacheKey = `${runtime.command}\0${runtime.version ?? ""}\0${selectedIdentity}\0${binaryStatFingerprint}`;
1131
+ const cached = modeHintCapabilityCache.get(cacheKey);
1132
+ if (cached !== undefined) return cached;
1133
+ let sawBinary = false;
1134
+ for (const candidate of candidates) {
1135
+ try {
1136
+ if (!existsSync(candidate)) continue;
1137
+ const buf = readFileSync(candidate);
1138
+ if (!isNativeExecutable(buf)) continue;
1139
+ sawBinary = true;
1140
+ if (buf.includes(Buffer.from("multi_agent_mode_hint_text", "utf8"))) {
1141
+ rememberModeHintCapability(cacheKey, true);
1142
+ return true;
1143
+ }
1144
+ } catch {
1145
+ continue;
1146
+ }
1147
+ }
1148
+ // At least one real binary was inspected and none contained the key.
1149
+ const result = sawBinary ? false : null;
1150
+ rememberModeHintCapability(cacheKey, result);
1151
+ return result;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+
1157
+ const CODEX_PLATFORM_PACKAGES = [
1158
+ ["codex-darwin-arm64", "aarch64-apple-darwin", "codex"],
1159
+ ["codex-darwin-x64", "x86_64-apple-darwin", "codex"],
1160
+ ["codex-linux-x64", "x86_64-unknown-linux-musl", "codex"],
1161
+ ["codex-linux-arm64", "aarch64-unknown-linux-musl", "codex"],
1162
+ ["codex-win32-x64", "x86_64-pc-windows-msvc", "codex.exe"],
1163
+ ["codex-win32-arm64", "aarch64-pc-windows-msvc", "codex.exe"],
1164
+ ] as const;
1165
+
1166
+ function isNativeExecutable(buf: Buffer): boolean {
1167
+ if (buf.length < 4) return false;
1168
+ if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE/COFF
1169
+ const magic = buf.readUInt32BE(0);
1170
+ return magic === 0x7f454c46 // ELF
1171
+ || magic === 0xfeedface || magic === 0xfeedfacf
1172
+ || magic === 0xcefaedfe || magic === 0xcffaedfe
1173
+ || magic === 0xcafebabe || magic === 0xbebafeca; // Mach-O/fat Mach-O
1174
+ }
1175
+
1176
+ /**
1177
+ * Candidate native codex binaries to probe. The resolved `command` may be the
1178
+ * opencodex shim or the npm JS wrapper (`codex.opencodex-real`), neither of
1179
+ * which embeds the Rust config schema. The real binary ships under the
1180
+ * platform package's `vendor/<triple>/bin/codex`; also try adjacent wrappers.
1181
+ */
1182
+ function codexNativeBinaryCandidates(command: string): string[] {
1183
+ const out = new Set<string>();
1184
+ const resolverBases = new Set<string>();
1185
+ const selectedPath = resolveSelectedCommandPath(command);
1186
+
1187
+ const addSelectedTarget = (target: string) => {
1188
+ try {
1189
+ const real = realpathSync(target);
1190
+ out.add(target);
1191
+ out.add(real);
1192
+ if (/[\\/]@openai[\\/]codex[\\/]bin[\\/]/.test(real)) resolverBases.add(real);
1193
+ } catch {
1194
+ // Missing backing paths cannot establish capability either way.
1195
+ }
1196
+ };
1197
+
1198
+ // Follow the resolved command to its real location. The opencodex shim and
1199
+ // the npm JS wrapper resolve to `@openai/codex/bin/codex.js`; the native
1200
+ // binary lives in the sibling platform package's vendor directory.
1201
+ if (selectedPath) {
1202
+ addSelectedTarget(selectedPath);
1203
+ for (const target of selectedShimBackingPaths(selectedPath)) addSelectedTarget(target);
1204
+ }
1205
+
1206
+ // npm's Windows wrappers are ordinary scripts. Resolve from both the global
1207
+ // prefix layout and a project-local node_modules/.bin layout; createRequire
1208
+ // then follows the selected installation's own dependency tree (including
1209
+ // pnpm/nested optional dependencies) without consulting unrelated PATH bins.
1210
+ const wrapperPath = selectedPath ?? command;
1211
+ if (/\.(?:cmd|ps1)$/i.test(wrapperPath)) {
1212
+ resolverBases.add(resolve(dirname(wrapperPath), "node_modules", "@openai", "codex", "bin", "codex.js"));
1213
+ resolverBases.add(resolve(dirname(wrapperPath), "..", "@openai", "codex", "bin", "codex.js"));
1214
+ }
1215
+
1216
+ for (const base of resolverBases) {
1217
+ const requireFromSelected = createRequire(base);
1218
+ for (const [pkg, triple, exe] of CODEX_PLATFORM_PACKAGES) {
1219
+ try {
1220
+ const manifest = requireFromSelected.resolve(`@openai/${pkg}/package.json`);
1221
+ out.add(join(dirname(manifest), "vendor", triple, "bin", exe));
1222
+ } catch {
1223
+ // Optional platform packages for other targets are normally absent.
1224
+ }
1225
+ }
1226
+ }
1227
+ return [...out];
1228
+ }
1229
+
1230
+ /** Backing paths tied to this exact OCX shim entry in codex-shim.json. */
1231
+ function selectedShimBackingPaths(commandPath: string): string[] {
1232
+ try {
1233
+ const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
1234
+ wrapperPath?: unknown;
1235
+ originalPath?: unknown;
1236
+ backupPath?: unknown;
1237
+ realPath?: unknown;
1238
+ wrappers?: Array<Record<string, unknown>>;
1239
+ };
1240
+ const entries = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
1241
+ const selected = resolve(commandPath);
1242
+ for (const entry of entries) {
1243
+ if (typeof entry.wrapperPath !== "string" || resolve(entry.wrapperPath) !== selected) continue;
1244
+ return [entry.backupPath, entry.realPath, entry.originalPath]
1245
+ .filter((value): value is string => typeof value === "string" && value.length > 0 && resolve(value) !== selected);
1246
+ }
1247
+ } catch {
1248
+ // Not an OCX-owned shim, or no readable state.
1249
+ }
1250
+ return [];
1251
+ }
1252
+
1253
+ /** Resolve only the selected bare command against PATH; never enumerate peers. */
1254
+ function resolveSelectedCommandPath(command: string): string | null {
1255
+ if (command.includes("/") || command.includes("\\")) return existsSync(command) ? resolve(command) : null;
1256
+ for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
1257
+ const candidate = join(dir, command);
1258
+ if (existsSync(candidate)) return candidate;
1259
+ }
1260
+ return null;
1261
+ }
1262
+
1263
+ function editAgentsMaxThreads(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
1264
+ const path = configPath ?? activeCodexConfigPath();
1265
+ const content = readConfigText(path);
1266
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
1267
+ const eol = dominantEol(content);
1268
+ const lines = content.split(/\r?\n/);
1269
+ const headerIdx = lines.findIndex(l => /^\s*\[agents\]\s*(?:#.*)?$/.test(l));
1270
+ if (headerIdx === -1) {
1271
+ if (value === null) return { ok: true, changed: false };
1272
+ const separator = lines.length > 0 && lines[lines.length - 1] !== "" ? [""] : [];
1273
+ lines.push(...separator, "[agents]", `max_threads = ${value}${migratedComment ?? ""}`);
1274
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1275
+ return { ok: true, changed: true };
1276
+ }
1277
+ let end = lines.length;
1278
+ for (let i = headerIdx + 1; i < lines.length; i++) {
1279
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
1280
+ }
1281
+ const keyRe = /^(\s*)max_threads\s*=\s*(\d+)(\s*#.*)?$/;
1282
+ for (let i = headerIdx + 1; i < end; i++) {
1283
+ const m = lines[i].match(keyRe);
1284
+ if (!m) continue;
1285
+ if (value === null) lines.splice(i, 1);
1286
+ else if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { ok: true, changed: false };
1287
+ else lines[i] = `${m[1]}max_threads = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
1288
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1289
+ return { ok: true, changed: true };
1290
+ }
1291
+ if (value === null) return { ok: true, changed: false };
1292
+ lines.splice(headerIdx + 1, 0, `max_threads = ${value}${migratedComment ?? ""}`);
1293
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1294
+ return { ok: true, changed: true };
1295
+ }
1296
+
1297
+ function removeMaxConcurrentThreads(configPath?: string): ConfigEditResult {
1298
+ const path = configPath ?? activeCodexConfigPath();
1299
+ const content = readConfigText(path);
1300
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
1301
+ const eol = dominantEol(content);
1302
+ const lines = content.split(/\r?\n/);
1303
+ const headerIdx = lines.findIndex(l => /^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/.test(l));
1304
+ if (headerIdx !== -1) {
1305
+ let end = lines.length;
1306
+ for (let i = headerIdx + 1; i < lines.length; i++) {
1307
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
1308
+ }
1309
+ const keyIdx = lines.findIndex((line, i) => i > headerIdx && i < end && /^\s*max_concurrent_threads_per_session\s*=/.test(line));
1310
+ if (keyIdx !== -1) {
1311
+ lines.splice(keyIdx, 1);
1312
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1313
+ return { ok: true, changed: true };
1314
+ }
1315
+ }
1316
+ const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
1317
+ if (featuresHeader === -1) return { ok: true, changed: false };
1318
+ let featuresEnd = lines.length;
1319
+ for (let i = featuresHeader + 1; i < lines.length; i++) {
1320
+ if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
1321
+ }
1322
+ const inlineRe = /^(\s*)multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)?$/;
1323
+ for (let i = featuresHeader + 1; i < featuresEnd; i++) {
1324
+ const inline = lines[i].match(inlineRe);
1325
+ if (!inline || !/(?:^|,)\s*max_concurrent_threads_per_session\s*=/.test(inline[2])) continue;
1326
+ const body = inline[2]
1327
+ .replace(/^\s*max_concurrent_threads_per_session\s*=\s*\d+\s*,?\s*/, "")
1328
+ .replace(/,\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, "")
1329
+ .trim();
1330
+ lines[i] = `${inline[1]}multi_agent_v2 = { ${body} }${inline[3] ?? ""}`;
1331
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
1332
+ return { ok: true, changed: true };
1333
+ }
1334
+ return { ok: true, changed: false };
1335
+ }
1336
+
1337
+ function ensureDisabledV2Config(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
1338
+ const path = configPath ?? activeCodexConfigPath();
1339
+ const content = readConfigText(path);
1340
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
1341
+ if (tomlTableBody(content, "features.multi_agent_v2") !== null || tomlTableBody(content, "features")?.match(/^\s*multi_agent_v2\s*=/m)) {
1342
+ if (value === null) return { ok: true, changed: false };
1343
+ return setMaxConcurrentThreads(value, path, migratedComment);
1344
+ }
1345
+ const eol = dominantEol(content);
1346
+ const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
1347
+ const table = `[features.multi_agent_v2]${eol}enabled = false${value === null ? "" : `${eol}max_concurrent_threads_per_session = ${value}${migratedComment ?? ""}`}${eol}`;
1348
+ atomicWriteFile(path, `${content}${suffix}${content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : ""}${table}`);
1349
+ return { ok: true, changed: true };
1350
+ }
1351
+
1352
+ /**
1353
+ * The effective concurrency limit expressed in the units of the currently ACTIVE
1354
+ * backend: under V2 the total-thread limit upstream enforces, under V1 the child
1355
+ * limit. The active backend's own key wins; the other backend's key is translated
1356
+ * across the root-agent slot. Display path only — never throws: a stored value
1357
+ * outside the translatable range is returned raw (at that magnitude the ±1 root
1358
+ * slot is already below float precision, and crashing `ocx v2 status` or
1359
+ * `GET /api/v2` is not a price worth paying for a translation that means nothing).
1360
+ * Migration code uses `discoverStoredThreadLimit` instead, which keeps provenance.
1361
+ */
1362
+ export function getLogicalMaxThreads(configPath?: string): number | null {
1363
+ if (isMultiAgentV2Enabled(configPath)) {
1364
+ const v2 = getMaxConcurrentThreads(configPath);
1365
+ if (v2 !== null) return v2;
1366
+ const legacy = getAgentsMaxThreads(configPath);
1367
+ if (legacy === null) return null;
1368
+ return isTranslatableV1ChildLimit(legacy) ? v1ChildLimitToV2TotalLimit(legacy) : legacy;
1369
+ }
1370
+ const legacy = getAgentsMaxThreads(configPath);
1371
+ if (legacy !== null) return legacy;
1372
+ const v2 = getMaxConcurrentThreads(configPath);
1373
+ if (v2 === null) return null;
1374
+ return isTranslatableV2TotalLimit(v2) ? v2TotalLimitToV1ChildLimit(v2) : v2;
1375
+ }
1376
+
1377
+ type ThreadLimitUnits = "v1-child" | "v2-total";
1378
+
1379
+ /**
1380
+ * Which storage the active limit lives in, in that storage's native units. The
1381
+ * active backend's own key wins; the other backend's key is the fallback and keeps
1382
+ * ITS units. Never translates and never throws. This is the migration-side sibling
1383
+ * of `getLogicalMaxThreads`: a migration needs to know exactly which storage the
1384
+ * value came from, and a display function's raw fallback would lose that.
1385
+ */
1386
+ function discoverStoredThreadLimit(configPath?: string): { value: number; units: ThreadLimitUnits } | null {
1387
+ if (isMultiAgentV2Enabled(configPath)) {
1388
+ const v2 = getMaxConcurrentThreads(configPath);
1389
+ if (v2 !== null) return { value: v2, units: "v2-total" };
1390
+ const legacy = getAgentsMaxThreads(configPath);
1391
+ return legacy === null ? null : { value: legacy, units: "v1-child" };
1392
+ }
1393
+ const legacy = getAgentsMaxThreads(configPath);
1394
+ if (legacy !== null) return { value: legacy, units: "v1-child" };
1395
+ const v2 = getMaxConcurrentThreads(configPath);
1396
+ return v2 === null ? null : { value: v2, units: "v2-total" };
1397
+ }
1398
+
1399
+ function activeThreadComment(content: string, v2Enabled: boolean): string | undefined {
1400
+ const legacy = tomlTableBody(content, "agents")?.match(/^\s*max_threads\s*=\s*\d+(\s*#.*)$/m)?.[1];
1401
+ const dedicated = tomlTableBody(content, "features.multi_agent_v2")
1402
+ ?.match(/^\s*max_concurrent_threads_per_session\s*=\s*\d+(\s*#.*)$/m)?.[1];
1403
+ const features = tomlTableBody(content, "features");
1404
+ const inlineLine = features?.match(/^\s*multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)$/m);
1405
+ const inline = inlineLine && /(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?:,|$)/.test(inlineLine[1])
1406
+ ? inlineLine[2]
1407
+ : undefined;
1408
+ return v2Enabled ? dedicated ?? inline ?? legacy : legacy ?? dedicated ?? inline;
1409
+ }
1410
+
1411
+ let migrationEditSeq = 0;
1412
+ /** Both residual classes gate the memo release: a plain residual and a
1413
+ * secret-bearing one alike keep their destination memo while the file
1414
+ * remains on disk. Exported for the regression seam. */
1415
+ export function isAtomicResidualError(error: unknown): boolean {
1416
+ return error instanceof AtomicWriteResidualTempError || error instanceof AtomicWriteSecretResidualError;
1417
+ }
1418
+
1419
+ function applyConfigEditsAtomically(path: string, edit: (tempPath: string) => ConfigEditResult): ConfigEditResult {
1420
+ const content = readConfigText(path);
1421
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
1422
+ const tempPath = `${path}.ocx-migration.${process.pid}.${++migrationEditSeq}`;
1423
+ // An inner residual temp (AtomicWriteResidualTempError) keeps its
1424
+ // destination-keyed memo: fail-closed while the residual exists.
1425
+ let innerResidual = false;
1426
+ try {
1427
+ atomicWriteFile(tempPath, content);
1428
+ const result = edit(tempPath);
1429
+ if (!result.ok) return result;
1430
+ const edited = readConfigText(tempPath);
1431
+ if (edited === null) return { ok: false, error: "temporary config migration output is unreadable" };
1432
+ if (edited === content) return { ok: true, changed: false };
1433
+ atomicWriteFile(path, edited);
1434
+ return { ok: true, changed: true };
1435
+ } catch (error) {
1436
+ if (isAtomicResidualError(error)) innerResidual = true;
1437
+ throw error;
1438
+ } finally {
1439
+ try {
1440
+ unlinkSync(tempPath);
1441
+ if (!innerResidual) forgetEphemeralSecretPath(tempPath);
1442
+ } catch (error) {
1443
+ // Already absent is also proven-absent; other failures keep the memo.
1444
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
1445
+ if (!innerResidual) forgetEphemeralSecretPath(tempPath);
1446
+ }
1447
+ }
1448
+ }
1449
+ }
1450
+
1451
+ export type MultiAgentV2TransitionResult =
1452
+ | { ok: true; changed: boolean; threadLimit: number | null }
1453
+ | { ok: false; error: string };
1454
+
1455
+ function transitionConfigError(content: string): string | null {
1456
+ if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.max_threads)\s*=/m.test(content)) {
1457
+ return "dotted multi-agent config keys are not supported for automatic migration";
1458
+ }
1459
+ const dedicatedTables = content.match(/^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/gm) ?? [];
1460
+ const featuresTables = content.match(/^\s*\[features\]\s*(?:#.*)?$/gm) ?? [];
1461
+ const agentsTables = content.match(/^\s*\[agents\]\s*(?:#.*)?$/gm) ?? [];
1462
+ if (dedicatedTables.length > 1 || featuresTables.length > 1 || agentsTables.length > 1) {
1463
+ return "duplicate multi-agent TOML tables cannot be migrated safely";
1464
+ }
1465
+ const features = tomlTableBody(content, "features");
1466
+ const featureDefs = features?.match(/^\s*multi_agent_v2\s*=/gm) ?? [];
1467
+ if (featureDefs.length > 1 || (dedicatedTables.length === 1 && featureDefs.length === 1)) {
1468
+ return "duplicate multi_agent_v2 definitions cannot be migrated safely";
1469
+ }
1470
+ if (features && /^\s*multi_agent_v2\.(?:enabled|max_concurrent_threads_per_session)\s*=/m.test(features)) {
1471
+ return "dotted multi_agent_v2 fields are not supported for automatic migration";
1472
+ }
1473
+ const agents = tomlTableBody(content, "agents");
1474
+ if ((agents?.match(/^\s*max_threads\s*=/gm) ?? []).length > 1) {
1475
+ return "duplicate agents.max_threads definitions cannot be migrated safely";
1476
+ }
1477
+ const dedicated = tomlTableBody(content, "features.multi_agent_v2");
1478
+ if ((dedicated?.match(/^\s*max_concurrent_threads_per_session\s*=/gm) ?? []).length > 1) {
1479
+ return "duplicate v2 thread-limit definitions cannot be migrated safely";
1480
+ }
1481
+ return null;
1482
+ }
1483
+
1484
+ /**
1485
+ * Toggle native multi_agent_v2 while moving the active thread limit to the key
1486
+ * valid for the destination version. Any failed command/postcondition restores
1487
+ * the exact original config bytes.
1488
+ */
1489
+ export function transitionMultiAgentV2(
1490
+ enabled: boolean,
1491
+ toggleFeature: (enabled: boolean) => void,
1492
+ options: { configPath?: string; threadLimit?: number } = {},
1493
+ ): MultiAgentV2TransitionResult {
1494
+ if (options.threadLimit !== undefined && (!Number.isInteger(options.threadLimit) || options.threadLimit < 1)) {
1495
+ return { ok: false, error: "thread limit must be an integer >= 1" };
1496
+ }
1497
+ const path = options.configPath ?? activeCodexConfigPath();
1498
+ const original = readConfigText(path);
1499
+ if (original === null) return { ok: false, error: `config.toml not readable at ${path}` };
1500
+ const preflightError = transitionConfigError(original);
1501
+ if (preflightError) return { ok: false, error: preflightError };
1502
+ const beforeEnabled = isMultiAgentV2Enabled(path);
1503
+ // A caller-supplied limit is already in the DESTINATION backend's units and is
1504
+ // never translated. A discovered limit carries the units of the storage it was
1505
+ // read from and crosses the root-slot boundary only when those units differ
1506
+ // from the destination's — which covers both backend flips and same-state
1507
+ // storage migrations (legacy-only under V2, V2-only under V1). The range
1508
+ // check runs only when a translation is actually needed, and before the try
1509
+ // block so an out-of-range stored value is a normal error result rather than
1510
+ // a RangeError escaping the rollback contract.
1511
+ const discovered = discoverStoredThreadLimit(path);
1512
+ const destinationUnits: ThreadLimitUnits = enabled ? "v2-total" : "v1-child";
1513
+ let threadLimit = options.threadLimit ?? discovered?.value ?? null;
1514
+ if (options.threadLimit === undefined && discovered !== null && discovered.units !== destinationUnits) {
1515
+ const translatable = discovered.units === "v1-child" ? isTranslatableV1ChildLimit : isTranslatableV2TotalLimit;
1516
+ if (!translatable(discovered.value)) {
1517
+ return { ok: false, error: `stored thread limit out of translatable range: ${discovered.value}` };
1518
+ }
1519
+ threadLimit = discovered.units === "v1-child"
1520
+ ? v1ChildLimitToV2TotalLimit(discovered.value)
1521
+ : v2TotalLimitToV1ChildLimit(discovered.value);
1522
+ }
1523
+ const migratedComment = activeThreadComment(original, beforeEnabled);
1524
+ try {
1525
+ if (enabled) {
1526
+ if (!beforeEnabled) {
1527
+ const staged = applyConfigEditsAtomically(path, tempPath => {
1528
+ const v2 = ensureDisabledV2Config(threadLimit, tempPath, migratedComment);
1529
+ if (!v2.ok) return v2;
1530
+ return editAgentsMaxThreads(null, tempPath);
1531
+ });
1532
+ if (!staged.ok) throw new Error(staged.error);
1533
+ toggleFeature(true);
1534
+ }
1535
+ if (!isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not enable multi_agent_v2");
1536
+ const target = applyConfigEditsAtomically(path, tempPath => {
1537
+ const v2 = threadLimit === null
1538
+ ? removeMaxConcurrentThreads(tempPath)
1539
+ : setMaxConcurrentThreads(threadLimit, tempPath, migratedComment);
1540
+ if (!v2.ok) return v2;
1541
+ return editAgentsMaxThreads(null, tempPath);
1542
+ });
1543
+ if (!target.ok) throw new Error(target.error);
1544
+ if (hasAgentsMaxThreads(path) || getMaxConcurrentThreads(path) !== threadLimit) throw new Error("v2 thread-limit migration postcondition failed");
1545
+ } else {
1546
+ if (beforeEnabled) toggleFeature(false);
1547
+ if (isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not disable multi_agent_v2");
1548
+ const target = applyConfigEditsAtomically(path, tempPath => {
1549
+ const v2 = removeMaxConcurrentThreads(tempPath);
1550
+ if (!v2.ok) return v2;
1551
+ return editAgentsMaxThreads(threadLimit, tempPath, migratedComment);
1552
+ });
1553
+ if (!target.ok) throw new Error(target.error);
1554
+ if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit) throw new Error("v1 thread-limit migration postcondition failed");
1555
+ }
1556
+ return { ok: true, changed: readConfigText(path) !== original, threadLimit };
1557
+ } catch (err) {
1558
+ const message = err instanceof Error ? err.message : String(err);
1559
+ try {
1560
+ atomicWriteFile(path, original);
1561
+ return { ok: false, error: message };
1562
+ } catch (rollbackErr) {
1563
+ return { ok: false, error: `${message}; rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}` };
1564
+ }
1565
+ }
1566
+ }