@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,969 @@
1
+ /**
2
+ * features.ts — codex feature-flag view for $CODEX_HOME/config.toml.
3
+ *
4
+ * Scope boundary: this module mirrors ONLY `multi_agent_v2`, because opencodex has
5
+ * to migrate its concurrency value across the v1/v2 boundary and expose the
6
+ * multi-agent config surface. Every other upstream feature flag is delegated to
7
+ * the native `codex features` command (see src/cli/v2.ts) and must not be
8
+ * hardcoded here.
9
+ *
10
+ * Upstream reshapes flags freely: in the 1f0566d3f..5a1097ed2 range alone,
11
+ * `code_mode_host` changed from a boolean to a table (it is Stage::Stable and
12
+ * default-enabled upstream), `enable_fanout` and `item_ids` were retired to
13
+ * Stage::Removed ("useless but kept for backward compatibility"), and several
14
+ * under-development flags were added. Delegation is what keeps opencodex out of
15
+ * that churn.
16
+ *
17
+ * Used by the catalog v2-gated-ultra policy (devlog/260709_v2_gated_ultra) and the
18
+ * `ocx v2` toggle surface. The FLAG itself is never written here — toggling goes
19
+ * through the official `codex features enable|disable` CLI (format-preserving).
20
+ * The one write this module owns is the numeric
21
+ * `features.multi_agent_v2.max_concurrent_threads_per_session` scalar
22
+ * (setMaxConcurrentThreads): the codex CLI has no persisted setter for nested
23
+ * feature config (`-c` is per-invocation only), so ocx does a scoped,
24
+ * EOL-preserving line edit — same practice as codex/inject.ts.
25
+ *
26
+ * CODEX_HOME is resolved at CALL time (activeCodexConfigPath pattern, mirrors
27
+ * catalog.ts:40-54) so tests can point fixtures via env or the explicit
28
+ * `configPath` parameter without fighting the module-load-time const in paths.ts.
29
+ */
30
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
31
+ import { join, resolve } from "node:path";
32
+ import { realpathSync } from "node:fs";
33
+ import { atomicWriteFile, expandUserPath } from "../config";
34
+ import { CODEX_CONFIG_PATH } from "./paths";
35
+
36
+ // EOL preservation, local copies of inject.ts dominantEol/applyEol: importing
37
+ // inject here would close a module cycle (features -> inject -> catalog -> features).
38
+ function dominantEol(content: string): "\r\n" | "\n" {
39
+ const crlf = (content.match(/\r\n/g) ?? []).length;
40
+ if (crlf === 0) return "\n";
41
+ const bareLf = (content.match(/\n/g) ?? []).length - crlf;
42
+ return crlf >= bareLf ? "\r\n" : "\n";
43
+ }
44
+
45
+ function applyEol(content: string, eol: "\r\n" | "\n"): string {
46
+ const normalized = content.replace(/\r\n/g, "\n");
47
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
48
+ }
49
+
50
+ function mergeTrailingComments(existing?: string, migrated?: string): string {
51
+ if (!existing) return migrated ?? "";
52
+ if (!migrated || existing.trim() === migrated.trim()) return existing;
53
+ const migratedText = migrated.replace(/^\s*#\s*/, "");
54
+ if (existing.replace(/^\s*#\s*/, "").split(";").map(part => part.trim()).includes(migratedText.trim())) return existing;
55
+ return `${existing}; ${migratedText}`;
56
+ }
57
+
58
+ function activeCodexConfigPath(): string {
59
+ const raw = process.env.CODEX_HOME?.trim();
60
+ if (!raw) return CODEX_CONFIG_PATH;
61
+ const path = resolve(expandUserPath(raw));
62
+ try {
63
+ return join(realpathSync.native(path), "config.toml");
64
+ } catch {
65
+ return join(path, "config.toml");
66
+ }
67
+ }
68
+
69
+ function readConfigText(configPath?: string): string | null {
70
+ const path = configPath ?? activeCodexConfigPath();
71
+ try {
72
+ if (!existsSync(path)) return null;
73
+ return readFileSync(path, "utf8");
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /** Body lines of a TOML table `[header]` up to (not including) the next table header. */
80
+ function tomlTableBody(content: string, header: string): string | null {
81
+ const lines = content.split("\n");
82
+ const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
83
+ const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
84
+ if (start === -1) return null;
85
+ const rest = lines.slice(start + 1);
86
+ const end = rest.findIndex(l => /^\s*\[/.test(l));
87
+ return (end === -1 ? rest : rest.slice(0, end)).join("\n");
88
+ }
89
+
90
+ function tomlBoolInBody(body: string, key: string): boolean | null {
91
+ const m = body.match(new RegExp(`^\\s*${key}\\s*=\\s*(true|false)\\s*(?:#.*)?$`, "m"));
92
+ return m ? m[1] === "true" : null;
93
+ }
94
+
95
+ /**
96
+ * TRUE when the codex `multi_agent_v2` feature is enabled in config.toml.
97
+ * Recognizes both shipped forms (codex-rs features/src/tests.rs):
98
+ * [features.multi_agent_v2] [features]
99
+ * enabled = true multi_agent_v2 = true
100
+ * plus the inline-table form `multi_agent_v2 = { enabled = true, ... }`.
101
+ * Missing file/key -> false (upstream default_enabled = false).
102
+ */
103
+ export function isMultiAgentV2Enabled(configPath?: string): boolean {
104
+ const content = readConfigText(configPath);
105
+ if (content === null) return false;
106
+
107
+ const table = tomlTableBody(content, "features.multi_agent_v2");
108
+ if (table !== null) {
109
+ const enabled = tomlBoolInBody(table, "enabled");
110
+ if (enabled !== null) return enabled;
111
+ // A bare [features.multi_agent_v2] table without `enabled` counts as on
112
+ // (FeatureToml::Config with enabled: None materializes as enabled upstream
113
+ // only when set; be conservative and require the boolean).
114
+ return false;
115
+ }
116
+
117
+ const features = tomlTableBody(content, "features");
118
+ if (features !== null) {
119
+ const bool = tomlBoolInBody(features, "multi_agent_v2");
120
+ if (bool !== null) return bool;
121
+ const inline = features.match(/^\s*multi_agent_v2\s*=\s*\{([^}]*)\}/m);
122
+ if (inline) {
123
+ const enabled = inline[1].match(/enabled\s*=\s*(true|false)/);
124
+ if (enabled) return enabled[1] === "true";
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ /**
131
+ * TRUE when config.toml still carries `[agents] max_threads` — codex-rs REFUSES to
132
+ * boot with that key while multi_agent_v2 is enabled ("agents.max_threads cannot be
133
+ * set when features.multi_agent_v2 is enabled", core/src/config/mod.rs:1421). The
134
+ * `ocx v2 on` flow warns about it instead of editing config itself.
135
+ */
136
+ export function hasAgentsMaxThreads(configPath?: string): boolean {
137
+ const content = readConfigText(configPath);
138
+ if (content === null) return false;
139
+ const agents = tomlTableBody(content, "agents");
140
+ if (agents === null) return false;
141
+ return /^\s*max_threads\s*=/m.test(agents);
142
+ }
143
+
144
+ /** Current legacy v1 `[agents] max_threads`, or null when absent/invalid. */
145
+ export function getAgentsMaxThreads(configPath?: string): number | null {
146
+ const content = readConfigText(configPath);
147
+ if (content === null) return null;
148
+ const agents = tomlTableBody(content, "agents");
149
+ if (agents === null) return null;
150
+ const m = agents.match(/^\s*max_threads\s*=\s*(\d+)\s*(?:#.*)?$/m);
151
+ if (!m) return null;
152
+ const value = Number(m[1]);
153
+ return Number.isInteger(value) && value >= 1 ? value : null;
154
+ }
155
+
156
+ /**
157
+ * Current `[agents] enabled`. Upstream defaults this to true and lets an enabled
158
+ * `features.multi_agent_v2` override it entirely (codex-rs core/src/config/mod.rs
159
+ * multi_agent_version_override returns V2 first at :1521-1523; `enabled = false`
160
+ * only takes effect with V2 off), so `null` means "unset, upstream default applies"
161
+ * and is NOT the same as `true`.
162
+ */
163
+ export function getAgentsEnabled(configPath?: string): boolean | null {
164
+ const content = readConfigText(configPath);
165
+ if (content === null) return null;
166
+ const agents = tomlTableBody(content, "agents");
167
+ if (agents === null) return null;
168
+ const m = agents.match(/^\s*enabled\s*=\s*(true|false)\s*(?:#.*)?$/m);
169
+ return m ? m[1] === "true" : null;
170
+ }
171
+
172
+ /**
173
+ * Current `[agents] max_depth`. Upstream applies this to V1 agent threads only and
174
+ * ignores it under V2 (config_toml.rs: "Maximum nesting depth for V1 agent threads.
175
+ * Ignored by V2."). The upstream type is `Option<i32>` with no minimum, so a
176
+ * negative value is valid config that effectively disables V1 child spawning —
177
+ * do not "correct" it, and do not present this as an effective V2 limit.
178
+ */
179
+ export function getAgentsMaxDepth(configPath?: string): number | null {
180
+ const content = readConfigText(configPath);
181
+ if (content === null) return null;
182
+ const agents = tomlTableBody(content, "agents");
183
+ if (agents === null) return null;
184
+ const m = agents.match(/^\s*max_depth\s*=\s*(-?\d+)\s*(?:#.*)?$/m);
185
+ if (!m) return null;
186
+ const value = Number(m[1]);
187
+ return Number.isInteger(value) && value >= -2_147_483_648 && value <= 2_147_483_647 ? value : null;
188
+ }
189
+
190
+ /**
191
+ * Current `features.multi_agent_v2.max_concurrent_threads_per_session`, from
192
+ * either the dedicated or inline-table form; null means the Codex default.
193
+ */
194
+ export function getMaxConcurrentThreads(configPath?: string): number | null {
195
+ const content = readConfigText(configPath);
196
+ if (content === null) return null;
197
+ const table = tomlTableBody(content, "features.multi_agent_v2");
198
+ const features = tomlTableBody(content, "features");
199
+ const inline = features?.match(/^\s*multi_agent_v2\s*=\s*\{([^}]*)\}/m);
200
+ const m = table?.match(/^\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?:#.*)?$/m)
201
+ ?? inline?.[1].match(/(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?:,|$)/);
202
+ if (!m) return null;
203
+ const value = Number(m[1]);
204
+ return Number.isFinite(value) && value >= 1 ? value : null;
205
+ }
206
+
207
+ /** Largest V1 child limit we translate. Well below Number.MAX_SAFE_INTEGER and far
208
+ * above any real concurrency setting; upstream's usize saturates, ours would silently
209
+ * lose precision. */
210
+ const MAX_TRANSLATABLE_V1_CHILD_LIMIT = 1_000_000;
211
+ /** The V2 side is one larger by construction: it counts the root agent's own slot, so
212
+ * the image of the maximum V1 value must itself be translatable back. */
213
+ const MAX_TRANSLATABLE_V2_TOTAL_LIMIT = MAX_TRANSLATABLE_V1_CHILD_LIMIT + 1;
214
+
215
+ export function isTranslatableV1ChildLimit(limit: number): boolean {
216
+ return Number.isInteger(limit) && limit >= 1 && limit <= MAX_TRANSLATABLE_V1_CHILD_LIMIT;
217
+ }
218
+
219
+ export function isTranslatableV2TotalLimit(limit: number): boolean {
220
+ return Number.isInteger(limit) && limit >= 1 && limit <= MAX_TRANSLATABLE_V2_TOTAL_LIMIT;
221
+ }
222
+
223
+ /**
224
+ * Upstream counts the root agent inside the V2 thread limit but not inside the legacy
225
+ * `[agents]` limit (codex-rs core/src/config/mod.rs resolve_multi_agent_v2_config applies
226
+ * saturating_add(1) to the [agents] value; the inverse saturating_sub(1) appears at
227
+ * mod.rs:1555). These helpers keep our migrations on the same side of that boundary.
228
+ */
229
+ export function v1ChildLimitToV2TotalLimit(childLimit: number): number {
230
+ if (!isTranslatableV1ChildLimit(childLimit)) {
231
+ throw new RangeError(`v1 child limit out of translatable range: ${childLimit}`);
232
+ }
233
+ return childLimit + 1;
234
+ }
235
+
236
+ /**
237
+ * Inverse of `v1ChildLimitToV2TotalLimit`. A V2 total of 1 means "root only, no
238
+ * children", which has no representable legacy child count >= 1, so it clamps to 1
239
+ * rather than writing 0 and tripping upstream's `>= 1` validation.
240
+ */
241
+ export function v2TotalLimitToV1ChildLimit(totalLimit: number): number {
242
+ if (!isTranslatableV2TotalLimit(totalLimit)) {
243
+ throw new RangeError(`v2 total limit out of translatable range: ${totalLimit}`);
244
+ }
245
+ return Math.max(1, totalLimit - 1);
246
+ }
247
+
248
+ /**
249
+ * Persist `features.multi_agent_v2.max_concurrent_threads_per_session = value`.
250
+ * Scoped edit in either the dedicated table or `[features]` boolean/inline form.
251
+ * Boolean form is upgraded to an inline config so the numeric value remains
252
+ * attached to the feature without a TOML key conflict. Idempotent on equal value.
253
+ */
254
+ export function setMaxConcurrentThreads(value: number, configPath?: string, migratedComment?: string): { ok: true; changed: boolean } | { ok: false; error: string } {
255
+ if (!Number.isInteger(value) || value < 1) {
256
+ return { ok: false, error: "max_concurrent_threads_per_session must be an integer >= 1" };
257
+ }
258
+ const path = configPath ?? activeCodexConfigPath();
259
+ const content = readConfigText(path);
260
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
261
+
262
+ const eol = dominantEol(content);
263
+ const lines = content.split(/\r?\n/);
264
+ const headerRe = /^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/;
265
+ const headerIdx = lines.findIndex(l => headerRe.test(l));
266
+ if (headerIdx === -1) {
267
+ const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
268
+ if (featuresHeader === -1) return { ok: false, error: "multi_agent_v2 feature config not found — enable v2 first (ocx v2 on)" };
269
+ let featuresEnd = lines.length;
270
+ for (let i = featuresHeader + 1; i < lines.length; i++) {
271
+ if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
272
+ }
273
+ const boolRe = /^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*#.*)?$/;
274
+ const inlineRe = /^(\s*)multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)?$/;
275
+ for (let i = featuresHeader + 1; i < featuresEnd; i++) {
276
+ const bool = lines[i].match(boolRe);
277
+ if (bool) {
278
+ lines[i] = `${bool[1]}multi_agent_v2 = { enabled = ${bool[2]}, max_concurrent_threads_per_session = ${value} }${mergeTrailingComments(bool[3], migratedComment)}`;
279
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
280
+ return { ok: true, changed: true };
281
+ }
282
+ const inline = lines[i].match(inlineRe);
283
+ if (!inline) continue;
284
+ const existing = inline[2].match(/(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?=,|$)/);
285
+ if (existing && Number(existing[1]) === value && (!migratedComment || migratedComment === inline[3])) return { ok: true, changed: false };
286
+ const body = existing
287
+ ? inline[2].replace(/(^|,)\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, `$1 max_concurrent_threads_per_session = ${value}`)
288
+ : `${inline[2].trim()}${inline[2].trim() ? ", " : ""}max_concurrent_threads_per_session = ${value}`;
289
+ lines[i] = `${inline[1]}multi_agent_v2 = { ${body.trim()} }${mergeTrailingComments(inline[3], migratedComment)}`;
290
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
291
+ return { ok: true, changed: true };
292
+ }
293
+ return { ok: false, error: "multi_agent_v2 feature config not found — enable v2 first (ocx v2 on)" };
294
+ }
295
+ let end = lines.length;
296
+ for (let i = headerIdx + 1; i < lines.length; i++) {
297
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
298
+ }
299
+ const keyRe = /^(\s*)max_concurrent_threads_per_session\s*=\s*(\d+)(\s*#.*)?$/;
300
+ for (let i = headerIdx + 1; i < end; i++) {
301
+ const m = lines[i].match(keyRe);
302
+ if (!m) continue;
303
+ if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { ok: true, changed: false };
304
+ lines[i] = `${m[1]}max_concurrent_threads_per_session = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
305
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
306
+ return { ok: true, changed: true };
307
+ }
308
+ lines.splice(headerIdx + 1, 0, `max_concurrent_threads_per_session = ${value}${migratedComment ?? ""}`);
309
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
310
+ return { ok: true, changed: true };
311
+ }
312
+
313
+ type ConfigEditResult = { ok: true; changed: boolean } | { ok: false; error: string };
314
+
315
+ /**
316
+ * Encode a string as a TOML single-line basic string.
317
+ *
318
+ * Character-by-character on purpose. A chained-replace implementation
319
+ * (`.replace(/\\/g, "\\\\").replace(/\t/g, "\\t")`) corrupts input: the backslash
320
+ * pass runs first, then later passes insert NEW backslashes the first pass can no
321
+ * longer protect. Single-line basic strings handle every case including embedded
322
+ * `"""`, which a multi-line `"""..."""` form cannot.
323
+ */
324
+ function encodeTomlBasicString(value: string): string {
325
+ let out = '"';
326
+ for (const ch of value) {
327
+ switch (ch) {
328
+ case "\\": out += "\\\\"; break;
329
+ case '"': out += '\\"'; break;
330
+ case "\n": out += "\\n"; break;
331
+ case "\r": out += "\\r"; break;
332
+ case "\t": out += "\\t"; break;
333
+ case "\b": out += "\\b"; break;
334
+ case "\f": out += "\\f"; break;
335
+ default: {
336
+ const code = ch.codePointAt(0)!;
337
+ out += code < 0x20 || code === 0x7f
338
+ ? `\\u${code.toString(16).padStart(4, "0")}`
339
+ : ch;
340
+ }
341
+ }
342
+ }
343
+ return out + '"';
344
+ }
345
+
346
+ /**
347
+ * Decode one TOML string token INCLUDING its quotes. Basic strings (`"..."`)
348
+ * unescape; literal strings (`'...'`) are verbatim — a backslash is not special
349
+ * there. Returns null for anything that is not a string token.
350
+ */
351
+ function decodeTomlStringToken(token: string): string | null {
352
+ if (token.length < 2) return null;
353
+ if (token.startsWith("'")) {
354
+ return token.endsWith("'") ? token.slice(1, -1) : null;
355
+ }
356
+ if (!token.startsWith('"') || !token.endsWith('"')) return null;
357
+ const body = token.slice(1, -1);
358
+ let out = "";
359
+ for (let i = 0; i < body.length; i++) {
360
+ const ch = body[i];
361
+ if (ch !== "\\") { out += ch; continue; }
362
+ const esc = body[++i];
363
+ switch (esc) {
364
+ case "\\": out += "\\"; break;
365
+ case '"': out += '"'; break;
366
+ case "n": out += "\n"; break;
367
+ case "r": out += "\r"; break;
368
+ case "t": out += "\t"; break;
369
+ case "b": out += "\b"; break;
370
+ case "f": out += "\f"; break;
371
+ case "u": {
372
+ const code = parseInt(body.slice(i + 1, i + 5), 16);
373
+ if (Number.isNaN(code)) return null;
374
+ out += String.fromCodePoint(code);
375
+ i += 4;
376
+ break;
377
+ }
378
+ case "U": {
379
+ const code = parseInt(body.slice(i + 1, i + 9), 16);
380
+ if (Number.isNaN(code)) return null;
381
+ out += String.fromCodePoint(code);
382
+ i += 8;
383
+ break;
384
+ }
385
+ default: return null;
386
+ }
387
+ }
388
+ return out;
389
+ }
390
+
391
+ /**
392
+ * End index (exclusive) of the TOML value starting at or after `start` in `text`.
393
+ * String-aware: basic strings honor backslash escapes, literal strings do not.
394
+ * Inline tables and arrays nest and are scanned with the same awareness.
395
+ */
396
+ function scanTomlValueEnd(text: string, start: number): number {
397
+ let i = start;
398
+ while (i < text.length && (text[i] === " " || text[i] === "\t")) i++;
399
+ const first = text[i];
400
+ if (first === '"') {
401
+ i++;
402
+ while (i < text.length) {
403
+ if (text[i] === "\\") { i += 2; continue; }
404
+ if (text[i] === '"') return i + 1;
405
+ i++;
406
+ }
407
+ return text.length;
408
+ }
409
+ if (first === "'") {
410
+ const close = text.indexOf("'", i + 1);
411
+ return close === -1 ? text.length : close + 1;
412
+ }
413
+ if (first === "{") {
414
+ const close = findInlineTableEnd(text, i);
415
+ return close === -1 ? text.length : close + 1;
416
+ }
417
+ if (first === "[") {
418
+ let depth = 0;
419
+ while (i < text.length) {
420
+ const c = text[i];
421
+ if (c === '"' || c === "'") { i = scanTomlValueEnd(text, i); continue; }
422
+ if (c === "[") depth++;
423
+ else if (c === "]") { depth--; if (depth === 0) return i + 1; }
424
+ i++;
425
+ }
426
+ return text.length;
427
+ }
428
+ while (i < text.length && !/[\s,}\]#]/.test(text[i])) i++;
429
+ return i;
430
+ }
431
+
432
+ /** Index of the `}` matching the `{` at `openIdx`, string-aware, or -1. */
433
+ function findInlineTableEnd(text: string, openIdx: number): number {
434
+ let depth = 0;
435
+ let i = openIdx;
436
+ while (i < text.length) {
437
+ const c = text[i];
438
+ if (c === '"' || c === "'") { i = scanTomlValueEnd(text, i); continue; }
439
+ if (c === "{") depth++;
440
+ else if (c === "}") { depth--; if (depth === 0) return i; }
441
+ i++;
442
+ }
443
+ return -1;
444
+ }
445
+
446
+ interface InlineEntry { keyStart: number; valueStart: number; valueEnd: number }
447
+
448
+ /**
449
+ * Locate `key = value` inside the inline-table body spanning [bodyStart, bodyEnd)
450
+ * (exclusive of the braces), string-aware on both keys and values, or null.
451
+ */
452
+ function findInlineEntry(text: string, bodyStart: number, bodyEnd: number, key: string): InlineEntry | null {
453
+ let i = bodyStart;
454
+ while (i < bodyEnd) {
455
+ while (i < bodyEnd && /[\s,]/.test(text[i])) i++;
456
+ if (i >= bodyEnd) break;
457
+ const entryStart = i;
458
+ let keyText: string;
459
+ if (text[i] === '"' || text[i] === "'") {
460
+ const keyEnd = scanTomlValueEnd(text, i);
461
+ keyText = decodeTomlStringToken(text.slice(i, keyEnd)) ?? "";
462
+ i = keyEnd;
463
+ } else {
464
+ const m = /^[A-Za-z0-9_-]+/.exec(text.slice(i, bodyEnd));
465
+ if (!m) break;
466
+ keyText = m[0];
467
+ i += m[0].length;
468
+ }
469
+ while (i < bodyEnd && /\s/.test(text[i])) i++;
470
+ if (text[i] !== "=") { i = entryStart + 1; continue; }
471
+ i++;
472
+ while (i < bodyEnd && /\s/.test(text[i])) i++;
473
+ const valueStart = i;
474
+ const valueEnd = Math.min(scanTomlValueEnd(text, valueStart), bodyEnd);
475
+ if (keyText === key) return { keyStart: entryStart, valueStart, valueEnd };
476
+ i = valueEnd;
477
+ }
478
+ return null;
479
+ }
480
+
481
+ /**
482
+ * Set or remove one scalar key inside a top-level TOML table, preserving every
483
+ * other line byte-for-byte — including the existing value's trailing comment,
484
+ * which is kept verbatim. `encoded` is the already-serialized RHS
485
+ * (`encodeTomlBasicString` for strings, `String(n)` for numbers, `"true"`/`"false"`
486
+ * for booleans); null removes the key. Creates the table when absent. Returns the
487
+ * new content; returning the input unchanged means no-op.
488
+ */
489
+ function editScalarInTable(content: string, table: string, key: string, encoded: string | null): string {
490
+ const eol = dominantEol(content);
491
+ const lines = content.split(/\r?\n/);
492
+ const headerRe = new RegExp(`^\\s*\\[${table.replace(/\./g, "\\.")}\\]\\s*(?:#.*)?$`);
493
+ const headerIdx = lines.findIndex(l => headerRe.test(l));
494
+ if (headerIdx === -1) {
495
+ if (encoded === null) return content;
496
+ const separator = lines.length > 0 && lines[lines.length - 1] !== "" ? [""] : [];
497
+ lines.push(...separator, `[${table}]`, `${key} = ${encoded}`);
498
+ return applyEol(lines.join("\n"), eol);
499
+ }
500
+ let end = lines.length;
501
+ for (let i = headerIdx + 1; i < lines.length; i++) {
502
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
503
+ }
504
+ const keyRe = new RegExp(`^(\\s*)${key}\\s*=\\s*`);
505
+ for (let i = headerIdx + 1; i < end; i++) {
506
+ const m = lines[i].match(keyRe);
507
+ if (!m) continue;
508
+ // The existing value may itself contain '#', so scan the value token
509
+ // string-aware instead of splitting on '#'.
510
+ const line = lines[i];
511
+ const valueStart = m[0].length;
512
+ const valueEnd = scanTomlValueEnd(line, valueStart);
513
+ const trailing = line.slice(valueEnd);
514
+ if (encoded === null) {
515
+ lines.splice(i, 1);
516
+ return applyEol(lines.join("\n"), eol);
517
+ }
518
+ if (line.slice(valueStart, valueEnd).trim() === encoded) return content;
519
+ lines[i] = `${m[1]}${key} = ${encoded}${trailing}`;
520
+ return applyEol(lines.join("\n"), eol);
521
+ }
522
+ if (encoded === null) return content;
523
+ lines.splice(headerIdx + 1, 0, `${key} = ${encoded}`);
524
+ return applyEol(lines.join("\n"), eol);
525
+ }
526
+
527
+ /** Persist `[agents] enabled = value`, or remove the key when `value` is null. */
528
+ export function setAgentsEnabled(value: boolean | null, configPath?: string): ConfigEditResult {
529
+ const path = configPath ?? activeCodexConfigPath();
530
+ const content = readConfigText(path);
531
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
532
+ const next = editScalarInTable(content, "agents", "enabled", value === null ? null : String(value));
533
+ if (next === content) return { ok: true, changed: false };
534
+ atomicWriteFile(path, next);
535
+ return { ok: true, changed: true };
536
+ }
537
+
538
+ /**
539
+ * Persist `[agents] max_depth = value`, or remove the key when `value` is null.
540
+ * Validation is exactly the upstream contract: `Option<i32>` with no minimum, so
541
+ * any integer in signed-i32 range is accepted — writing anything wider would
542
+ * produce a config upstream cannot deserialize, a hard parse failure for the
543
+ * user's Codex.
544
+ */
545
+ export function setAgentsMaxDepth(value: number | null, configPath?: string): ConfigEditResult {
546
+ if (value !== null && (!Number.isInteger(value) || value < -2_147_483_648 || value > 2_147_483_647)) {
547
+ return { ok: false, error: "max_depth must be an integer within signed i32 range" };
548
+ }
549
+ const path = configPath ?? activeCodexConfigPath();
550
+ const content = readConfigText(path);
551
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
552
+ const next = editScalarInTable(content, "agents", "max_depth", value === null ? null : String(value));
553
+ if (next === content) return { ok: true, changed: false };
554
+ atomicWriteFile(path, next);
555
+ return { ok: true, changed: true };
556
+ }
557
+
558
+ /**
559
+ * Current `features.multi_agent_v2.subagent_developer_instructions`.
560
+ *
561
+ * Upstream tri-state (codex-rs core/src/config/mod.rs resolve_multi_agent_v2_config):
562
+ * unset -> the child inherits the parent's developer instructions
563
+ * non-empty -> replaces the inherited parent fragment
564
+ * empty string -> clears the inherited fragment
565
+ *
566
+ * So `null` and `""` are DIFFERENT values and both must round-trip. Upstream
567
+ * `.trim()`s the configured text, so whitespace-only values are effectively `""`.
568
+ * Reads both the dedicated-table and inline forms; dedicated wins, mirroring
569
+ * `getMaxConcurrentThreads` precedence.
570
+ */
571
+ export function getSubagentDeveloperInstructions(configPath?: string): string | null {
572
+ const content = readConfigText(configPath);
573
+ if (content === null) return null;
574
+ const table = tomlTableBody(content, "features.multi_agent_v2");
575
+ if (table !== null) {
576
+ const m = table.match(/^\s*subagent_developer_instructions\s*=\s*/m);
577
+ if (m) {
578
+ const valueStart = m.index! + m[0].length;
579
+ const token = table.slice(valueStart, scanTomlValueEnd(table, valueStart)).trim();
580
+ return decodeTomlStringToken(token);
581
+ }
582
+ return null;
583
+ }
584
+ const features = tomlTableBody(content, "features");
585
+ if (features === null) return null;
586
+ const m = features.match(/multi_agent_v2\s*=\s*\{/);
587
+ if (!m) return null;
588
+ const openIdx = m.index! + m[0].length - 1;
589
+ const closeIdx = findInlineTableEnd(features, openIdx);
590
+ if (closeIdx === -1) return null;
591
+ const entry = findInlineEntry(features, openIdx + 1, closeIdx, "subagent_developer_instructions");
592
+ if (!entry) return null;
593
+ return decodeTomlStringToken(features.slice(entry.valueStart, entry.valueEnd).trim());
594
+ }
595
+
596
+ /**
597
+ * Persist `features.multi_agent_v2.subagent_developer_instructions`, or remove the
598
+ * key when `value` is null. Handles all three existing encodings: the dedicated
599
+ * table (scalar edit), the inline table (string-aware edit inside the braces — a
600
+ * regex over `[^}]*` would corrupt any value containing `}`), and the bare boolean
601
+ * form (upgraded in place to an inline table, mirroring `setMaxConcurrentThreads`).
602
+ * With no existing v2 config, creates the dedicated table carrying only this key.
603
+ * The key name must match upstream character-for-character: the upstream struct
604
+ * carries `#[serde(deny_unknown_fields)]`, so a misspelling is not ignored — it is
605
+ * a hard config-parse failure for the user's Codex.
606
+ */
607
+ export function setSubagentDeveloperInstructions(value: string | null, configPath?: string): ConfigEditResult {
608
+ const path = configPath ?? activeCodexConfigPath();
609
+ const content = readConfigText(path);
610
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
611
+ const encoded = value === null ? null : encodeTomlBasicString(value);
612
+
613
+ if (tomlTableBody(content, "features.multi_agent_v2") !== null) {
614
+ const next = editScalarInTable(content, "features.multi_agent_v2", "subagent_developer_instructions", encoded);
615
+ if (next === content) return { ok: true, changed: false };
616
+ atomicWriteFile(path, next);
617
+ return { ok: true, changed: true };
618
+ }
619
+
620
+ const eol = dominantEol(content);
621
+ const lines = content.split(/\r?\n/);
622
+ const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
623
+ if (featuresHeader !== -1) {
624
+ let featuresEnd = lines.length;
625
+ for (let i = featuresHeader + 1; i < lines.length; i++) {
626
+ if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
627
+ }
628
+ for (let i = featuresHeader + 1; i < featuresEnd; i++) {
629
+ const line = lines[i];
630
+ const inlineMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*\{/);
631
+ if (inlineMatch) {
632
+ const openIdx = inlineMatch[0].length - 1;
633
+ const closeIdx = findInlineTableEnd(line, openIdx);
634
+ if (closeIdx === -1) return { ok: false, error: "malformed multi_agent_v2 inline table" };
635
+ const entry = findInlineEntry(line, openIdx + 1, closeIdx, "subagent_developer_instructions");
636
+ if (encoded === null) {
637
+ if (!entry) return { ok: true, changed: false };
638
+ let start = entry.keyStart;
639
+ let stop = entry.valueEnd;
640
+ let j = stop;
641
+ while (j < closeIdx && line[j] === " ") j++;
642
+ if (line[j] === ",") {
643
+ stop = j + 1;
644
+ while (stop < closeIdx && line[stop] === " ") stop++;
645
+ } else {
646
+ let k = start;
647
+ while (k > openIdx + 1 && line[k - 1] === " ") k--;
648
+ if (line[k - 1] === ",") start = k - 1;
649
+ }
650
+ lines[i] = line.slice(0, start) + line.slice(stop);
651
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
652
+ return { ok: true, changed: true };
653
+ }
654
+ if (entry) {
655
+ if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return { ok: true, changed: false };
656
+ lines[i] = line.slice(0, entry.valueStart) + encoded + line.slice(entry.valueEnd);
657
+ } else {
658
+ let insertPos = closeIdx;
659
+ while (insertPos > openIdx + 1 && line[insertPos - 1] === " ") insertPos--;
660
+ const hasEntries = line.slice(openIdx + 1, insertPos).trim().length > 0;
661
+ const insertion = hasEntries
662
+ ? `, subagent_developer_instructions = ${encoded} `
663
+ : ` subagent_developer_instructions = ${encoded} `;
664
+ lines[i] = line.slice(0, insertPos) + insertion + line.slice(closeIdx);
665
+ }
666
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
667
+ return { ok: true, changed: true };
668
+ }
669
+ const boolMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*(?:#.*)?)$/);
670
+ if (boolMatch) {
671
+ if (encoded === null) return { ok: true, changed: false };
672
+ lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]}, subagent_developer_instructions = ${encoded} }${boolMatch[3]}`;
673
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
674
+ return { ok: true, changed: true };
675
+ }
676
+ }
677
+ }
678
+
679
+ if (encoded === null) return { ok: true, changed: false };
680
+ const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
681
+ const separator = content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : "";
682
+ const tableText = `[features.multi_agent_v2]${eol}subagent_developer_instructions = ${encoded}${eol}`;
683
+ atomicWriteFile(path, `${content}${suffix}${separator}${tableText}`);
684
+ return { ok: true, changed: true };
685
+ }
686
+
687
+ function editAgentsMaxThreads(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
688
+ const path = configPath ?? activeCodexConfigPath();
689
+ const content = readConfigText(path);
690
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
691
+ const eol = dominantEol(content);
692
+ const lines = content.split(/\r?\n/);
693
+ const headerIdx = lines.findIndex(l => /^\s*\[agents\]\s*(?:#.*)?$/.test(l));
694
+ if (headerIdx === -1) {
695
+ if (value === null) return { ok: true, changed: false };
696
+ const separator = lines.length > 0 && lines[lines.length - 1] !== "" ? [""] : [];
697
+ lines.push(...separator, "[agents]", `max_threads = ${value}${migratedComment ?? ""}`);
698
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
699
+ return { ok: true, changed: true };
700
+ }
701
+ let end = lines.length;
702
+ for (let i = headerIdx + 1; i < lines.length; i++) {
703
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
704
+ }
705
+ const keyRe = /^(\s*)max_threads\s*=\s*(\d+)(\s*#.*)?$/;
706
+ for (let i = headerIdx + 1; i < end; i++) {
707
+ const m = lines[i].match(keyRe);
708
+ if (!m) continue;
709
+ if (value === null) lines.splice(i, 1);
710
+ else if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { ok: true, changed: false };
711
+ else lines[i] = `${m[1]}max_threads = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
712
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
713
+ return { ok: true, changed: true };
714
+ }
715
+ if (value === null) return { ok: true, changed: false };
716
+ lines.splice(headerIdx + 1, 0, `max_threads = ${value}${migratedComment ?? ""}`);
717
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
718
+ return { ok: true, changed: true };
719
+ }
720
+
721
+ function removeMaxConcurrentThreads(configPath?: string): ConfigEditResult {
722
+ const path = configPath ?? activeCodexConfigPath();
723
+ const content = readConfigText(path);
724
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
725
+ const eol = dominantEol(content);
726
+ const lines = content.split(/\r?\n/);
727
+ const headerIdx = lines.findIndex(l => /^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/.test(l));
728
+ if (headerIdx !== -1) {
729
+ let end = lines.length;
730
+ for (let i = headerIdx + 1; i < lines.length; i++) {
731
+ if (/^\s*\[/.test(lines[i])) { end = i; break; }
732
+ }
733
+ const keyIdx = lines.findIndex((line, i) => i > headerIdx && i < end && /^\s*max_concurrent_threads_per_session\s*=/.test(line));
734
+ if (keyIdx !== -1) {
735
+ lines.splice(keyIdx, 1);
736
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
737
+ return { ok: true, changed: true };
738
+ }
739
+ }
740
+ const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
741
+ if (featuresHeader === -1) return { ok: true, changed: false };
742
+ let featuresEnd = lines.length;
743
+ for (let i = featuresHeader + 1; i < lines.length; i++) {
744
+ if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
745
+ }
746
+ const inlineRe = /^(\s*)multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)?$/;
747
+ for (let i = featuresHeader + 1; i < featuresEnd; i++) {
748
+ const inline = lines[i].match(inlineRe);
749
+ if (!inline || !/(?:^|,)\s*max_concurrent_threads_per_session\s*=/.test(inline[2])) continue;
750
+ const body = inline[2]
751
+ .replace(/^\s*max_concurrent_threads_per_session\s*=\s*\d+\s*,?\s*/, "")
752
+ .replace(/,\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, "")
753
+ .trim();
754
+ lines[i] = `${inline[1]}multi_agent_v2 = { ${body} }${inline[3] ?? ""}`;
755
+ atomicWriteFile(path, applyEol(lines.join("\n"), eol));
756
+ return { ok: true, changed: true };
757
+ }
758
+ return { ok: true, changed: false };
759
+ }
760
+
761
+ function ensureDisabledV2Config(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
762
+ const path = configPath ?? activeCodexConfigPath();
763
+ const content = readConfigText(path);
764
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
765
+ if (tomlTableBody(content, "features.multi_agent_v2") !== null || tomlTableBody(content, "features")?.match(/^\s*multi_agent_v2\s*=/m)) {
766
+ if (value === null) return { ok: true, changed: false };
767
+ return setMaxConcurrentThreads(value, path, migratedComment);
768
+ }
769
+ const eol = dominantEol(content);
770
+ const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
771
+ const table = `[features.multi_agent_v2]${eol}enabled = false${value === null ? "" : `${eol}max_concurrent_threads_per_session = ${value}${migratedComment ?? ""}`}${eol}`;
772
+ atomicWriteFile(path, `${content}${suffix}${content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : ""}${table}`);
773
+ return { ok: true, changed: true };
774
+ }
775
+
776
+ /**
777
+ * The effective concurrency limit expressed in the units of the currently ACTIVE
778
+ * backend: under V2 the total-thread limit upstream enforces, under V1 the child
779
+ * limit. The active backend's own key wins; the other backend's key is translated
780
+ * across the root-agent slot. Display path only — never throws: a stored value
781
+ * outside the translatable range is returned raw (at that magnitude the ±1 root
782
+ * slot is already below float precision, and crashing `ocx v2 status` or
783
+ * `GET /api/v2` is not a price worth paying for a translation that means nothing).
784
+ * Migration code uses `discoverStoredThreadLimit` instead, which keeps provenance.
785
+ */
786
+ export function getLogicalMaxThreads(configPath?: string): number | null {
787
+ if (isMultiAgentV2Enabled(configPath)) {
788
+ const v2 = getMaxConcurrentThreads(configPath);
789
+ if (v2 !== null) return v2;
790
+ const legacy = getAgentsMaxThreads(configPath);
791
+ if (legacy === null) return null;
792
+ return isTranslatableV1ChildLimit(legacy) ? v1ChildLimitToV2TotalLimit(legacy) : legacy;
793
+ }
794
+ const legacy = getAgentsMaxThreads(configPath);
795
+ if (legacy !== null) return legacy;
796
+ const v2 = getMaxConcurrentThreads(configPath);
797
+ if (v2 === null) return null;
798
+ return isTranslatableV2TotalLimit(v2) ? v2TotalLimitToV1ChildLimit(v2) : v2;
799
+ }
800
+
801
+ type ThreadLimitUnits = "v1-child" | "v2-total";
802
+
803
+ /**
804
+ * Which storage the active limit lives in, in that storage's native units. The
805
+ * active backend's own key wins; the other backend's key is the fallback and keeps
806
+ * ITS units. Never translates and never throws. This is the migration-side sibling
807
+ * of `getLogicalMaxThreads`: a migration needs to know exactly which storage the
808
+ * value came from, and a display function's raw fallback would lose that.
809
+ */
810
+ function discoverStoredThreadLimit(configPath?: string): { value: number; units: ThreadLimitUnits } | null {
811
+ if (isMultiAgentV2Enabled(configPath)) {
812
+ const v2 = getMaxConcurrentThreads(configPath);
813
+ if (v2 !== null) return { value: v2, units: "v2-total" };
814
+ const legacy = getAgentsMaxThreads(configPath);
815
+ return legacy === null ? null : { value: legacy, units: "v1-child" };
816
+ }
817
+ const legacy = getAgentsMaxThreads(configPath);
818
+ if (legacy !== null) return { value: legacy, units: "v1-child" };
819
+ const v2 = getMaxConcurrentThreads(configPath);
820
+ return v2 === null ? null : { value: v2, units: "v2-total" };
821
+ }
822
+
823
+ function activeThreadComment(content: string, v2Enabled: boolean): string | undefined {
824
+ const legacy = tomlTableBody(content, "agents")?.match(/^\s*max_threads\s*=\s*\d+(\s*#.*)$/m)?.[1];
825
+ const dedicated = tomlTableBody(content, "features.multi_agent_v2")
826
+ ?.match(/^\s*max_concurrent_threads_per_session\s*=\s*\d+(\s*#.*)$/m)?.[1];
827
+ const features = tomlTableBody(content, "features");
828
+ const inlineLine = features?.match(/^\s*multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)$/m);
829
+ const inline = inlineLine && /(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?:,|$)/.test(inlineLine[1])
830
+ ? inlineLine[2]
831
+ : undefined;
832
+ return v2Enabled ? dedicated ?? inline ?? legacy : legacy ?? dedicated ?? inline;
833
+ }
834
+
835
+ let migrationEditSeq = 0;
836
+ function applyConfigEditsAtomically(path: string, edit: (tempPath: string) => ConfigEditResult): ConfigEditResult {
837
+ const content = readConfigText(path);
838
+ if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
839
+ const tempPath = `${path}.ocx-migration.${process.pid}.${++migrationEditSeq}`;
840
+ try {
841
+ atomicWriteFile(tempPath, content);
842
+ const result = edit(tempPath);
843
+ if (!result.ok) return result;
844
+ const edited = readConfigText(tempPath);
845
+ if (edited === null) return { ok: false, error: "temporary config migration output is unreadable" };
846
+ if (edited === content) return { ok: true, changed: false };
847
+ atomicWriteFile(path, edited);
848
+ return { ok: true, changed: true };
849
+ } finally {
850
+ try { unlinkSync(tempPath); } catch { /* already absent */ }
851
+ }
852
+ }
853
+
854
+ export type MultiAgentV2TransitionResult =
855
+ | { ok: true; changed: boolean; threadLimit: number | null }
856
+ | { ok: false; error: string };
857
+
858
+ function transitionConfigError(content: string): string | null {
859
+ if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.max_threads)\s*=/m.test(content)) {
860
+ return "dotted multi-agent config keys are not supported for automatic migration";
861
+ }
862
+ const dedicatedTables = content.match(/^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/gm) ?? [];
863
+ const featuresTables = content.match(/^\s*\[features\]\s*(?:#.*)?$/gm) ?? [];
864
+ const agentsTables = content.match(/^\s*\[agents\]\s*(?:#.*)?$/gm) ?? [];
865
+ if (dedicatedTables.length > 1 || featuresTables.length > 1 || agentsTables.length > 1) {
866
+ return "duplicate multi-agent TOML tables cannot be migrated safely";
867
+ }
868
+ const features = tomlTableBody(content, "features");
869
+ const featureDefs = features?.match(/^\s*multi_agent_v2\s*=/gm) ?? [];
870
+ if (featureDefs.length > 1 || (dedicatedTables.length === 1 && featureDefs.length === 1)) {
871
+ return "duplicate multi_agent_v2 definitions cannot be migrated safely";
872
+ }
873
+ if (features && /^\s*multi_agent_v2\.(?:enabled|max_concurrent_threads_per_session)\s*=/m.test(features)) {
874
+ return "dotted multi_agent_v2 fields are not supported for automatic migration";
875
+ }
876
+ const agents = tomlTableBody(content, "agents");
877
+ if ((agents?.match(/^\s*max_threads\s*=/gm) ?? []).length > 1) {
878
+ return "duplicate agents.max_threads definitions cannot be migrated safely";
879
+ }
880
+ const dedicated = tomlTableBody(content, "features.multi_agent_v2");
881
+ if ((dedicated?.match(/^\s*max_concurrent_threads_per_session\s*=/gm) ?? []).length > 1) {
882
+ return "duplicate v2 thread-limit definitions cannot be migrated safely";
883
+ }
884
+ return null;
885
+ }
886
+
887
+ /**
888
+ * Toggle native multi_agent_v2 while moving the active thread limit to the key
889
+ * valid for the destination version. Any failed command/postcondition restores
890
+ * the exact original config bytes.
891
+ */
892
+ export function transitionMultiAgentV2(
893
+ enabled: boolean,
894
+ toggleFeature: (enabled: boolean) => void,
895
+ options: { configPath?: string; threadLimit?: number } = {},
896
+ ): MultiAgentV2TransitionResult {
897
+ if (options.threadLimit !== undefined && (!Number.isInteger(options.threadLimit) || options.threadLimit < 1)) {
898
+ return { ok: false, error: "thread limit must be an integer >= 1" };
899
+ }
900
+ const path = options.configPath ?? activeCodexConfigPath();
901
+ const original = readConfigText(path);
902
+ if (original === null) return { ok: false, error: `config.toml not readable at ${path}` };
903
+ const preflightError = transitionConfigError(original);
904
+ if (preflightError) return { ok: false, error: preflightError };
905
+ const beforeEnabled = isMultiAgentV2Enabled(path);
906
+ // A caller-supplied limit is already in the DESTINATION backend's units and is
907
+ // never translated. A discovered limit carries the units of the storage it was
908
+ // read from and crosses the root-slot boundary only when those units differ
909
+ // from the destination's — which covers both backend flips and same-state
910
+ // storage migrations (legacy-only under V2, V2-only under V1). The range
911
+ // check runs only when a translation is actually needed, and before the try
912
+ // block so an out-of-range stored value is a normal error result rather than
913
+ // a RangeError escaping the rollback contract.
914
+ const discovered = discoverStoredThreadLimit(path);
915
+ const destinationUnits: ThreadLimitUnits = enabled ? "v2-total" : "v1-child";
916
+ let threadLimit = options.threadLimit ?? discovered?.value ?? null;
917
+ if (options.threadLimit === undefined && discovered !== null && discovered.units !== destinationUnits) {
918
+ const translatable = discovered.units === "v1-child" ? isTranslatableV1ChildLimit : isTranslatableV2TotalLimit;
919
+ if (!translatable(discovered.value)) {
920
+ return { ok: false, error: `stored thread limit out of translatable range: ${discovered.value}` };
921
+ }
922
+ threadLimit = discovered.units === "v1-child"
923
+ ? v1ChildLimitToV2TotalLimit(discovered.value)
924
+ : v2TotalLimitToV1ChildLimit(discovered.value);
925
+ }
926
+ const migratedComment = activeThreadComment(original, beforeEnabled);
927
+ try {
928
+ if (enabled) {
929
+ if (!beforeEnabled) {
930
+ const staged = applyConfigEditsAtomically(path, tempPath => {
931
+ const v2 = ensureDisabledV2Config(threadLimit, tempPath, migratedComment);
932
+ if (!v2.ok) return v2;
933
+ return editAgentsMaxThreads(null, tempPath);
934
+ });
935
+ if (!staged.ok) throw new Error(staged.error);
936
+ toggleFeature(true);
937
+ }
938
+ if (!isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not enable multi_agent_v2");
939
+ const target = applyConfigEditsAtomically(path, tempPath => {
940
+ const v2 = threadLimit === null
941
+ ? removeMaxConcurrentThreads(tempPath)
942
+ : setMaxConcurrentThreads(threadLimit, tempPath, migratedComment);
943
+ if (!v2.ok) return v2;
944
+ return editAgentsMaxThreads(null, tempPath);
945
+ });
946
+ if (!target.ok) throw new Error(target.error);
947
+ if (hasAgentsMaxThreads(path) || getMaxConcurrentThreads(path) !== threadLimit) throw new Error("v2 thread-limit migration postcondition failed");
948
+ } else {
949
+ if (beforeEnabled) toggleFeature(false);
950
+ if (isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not disable multi_agent_v2");
951
+ const target = applyConfigEditsAtomically(path, tempPath => {
952
+ const v2 = removeMaxConcurrentThreads(tempPath);
953
+ if (!v2.ok) return v2;
954
+ return editAgentsMaxThreads(threadLimit, tempPath, migratedComment);
955
+ });
956
+ if (!target.ok) throw new Error(target.error);
957
+ if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit) throw new Error("v1 thread-limit migration postcondition failed");
958
+ }
959
+ return { ok: true, changed: readConfigText(path) !== original, threadLimit };
960
+ } catch (err) {
961
+ const message = err instanceof Error ? err.message : String(err);
962
+ try {
963
+ atomicWriteFile(path, original);
964
+ return { ok: false, error: message };
965
+ } catch (rollbackErr) {
966
+ return { ok: false, error: `${message}; rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}` };
967
+ }
968
+ }
969
+ }