@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -3,11 +3,16 @@ import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-sta
3
3
  import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { readFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
+ import { prepareCodexHome } from "../../codex/prepared-home";
8
+ import { codexHomeIsAbsent } from "../../codex/codex-home-owner";
6
9
  import type { CatalogModel } from "../../codex/catalog";
7
10
  import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
8
11
  import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
9
12
  import { MULTI_AGENT_SURFACE_ADVISORY_VERSION, multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../../config/multi-agent-surface";
10
13
  import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
14
+ import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
15
+ import { isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance-id";
11
16
  import {
12
17
  adoptPersistedClaudeCode,
13
18
  DEFAULT_SUBAGENT_MODELS,
@@ -71,9 +76,13 @@ import {
71
76
  webSearchCandidateRows,
72
77
  webSearchModelIsRejected,
73
78
  webSearchModelRejection,
79
+ sidecarAnthropicPoolOptions,
80
+ sidecarSettingsAfterPatch,
81
+ sidecarOptionsAuth,
74
82
  type WebSearchBackend,
75
83
  } from "./web-search-sidecar-options";
76
84
  import { drainAndShutdown } from "../lifecycle";
85
+ import { inheritedAnthropicInstance } from "../../sidecar/auth";
77
86
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
78
87
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
79
88
  import type { PersistedUsageAttempt } from "../../usage/log";
@@ -394,6 +403,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
394
403
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
395
404
  getMultiAgentModeHintText, probeCodexSupportsModeHint, setAgentsEnabled, setAgentsMaxDepth,
396
405
  setSubagentDeveloperInstructions, setMultiAgentModeHintText, MODE_HINT_UNSUPPORTED_ERROR,
406
+ activeCodexConfigPath,
397
407
  } = await import("../../codex/features");
398
408
  // Probe the capability before any combined-request mutation. The scalar writer
399
409
  // repeats this check, but doing it here prevents an earlier flag/mode/agents
@@ -413,17 +423,37 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
413
423
  const requestedFlag = wantsFlag
414
424
  ? body.enabled as boolean
415
425
  : modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
426
+ /*
427
+ * One held section for every config.toml write this PUT makes — the
428
+ * feature transition plus each scalar edit. Individually-locked writers
429
+ * would let a foreign write land between them and leave a half-applied
430
+ * combination on disk; the lock is released before the post-write readers
431
+ * and catalog converge below.
432
+ */
433
+ const needsCodexConfigWrites = requestedFlag !== undefined || wantsThreads
434
+ || wantsAgentsEnabled || wantsMaxDepth || wantsSubagentInstructions || wantsModeHintText;
435
+ if (needsCodexConfigWrites && codexHomeIsAbsent(dirname(activeCodexConfigPath()))) {
436
+ return jsonResponse({ error: `config.toml not readable at ${activeCodexConfigPath()}` }, 502);
437
+ }
438
+ const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage } = await import("../../codex/config-write-lock");
439
+ const configWriteLock = needsCodexConfigWrites ? await acquireConfigWriteLock(activeCodexConfigPath()) : null;
440
+ if (configWriteLock !== null && !configWriteLock.ok) {
441
+ return jsonResponse({ error: configWriteLockFailureMessage(configWriteLock), retryable: configWriteLock.error === "locked" }, 502);
442
+ }
443
+ const heldWriteLock = configWriteLock !== null && configWriteLock.ok ? configWriteLock.handle : undefined;
444
+ try {
416
445
  if (requestedFlag !== undefined || wantsThreads) {
417
446
  const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
418
447
  let toggle = deps.toggleCodexMultiAgentV2;
419
448
  if (!toggle) {
420
449
  const { runCodexFeaturesCommand } = await import("../../cli/v2");
421
- toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable");
450
+ toggle = (enabled, env, validate) => runCodexFeaturesCommand(enabled ? "enable" : "disable", "multi_agent_v2", env, validate);
422
451
  }
423
452
  const result = transitionMultiAgentV2(targetFlag, toggle, {
424
453
  ...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
454
+ heldConfigWriteLock: heldWriteLock,
425
455
  });
426
- if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}` }, 502);
456
+ if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}`, ...(result.retryable === false ? { retryable: false } : {}) }, 502);
427
457
  if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
428
458
  }
429
459
  if (wantsMode) {
@@ -459,10 +489,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
459
489
  // asserts this route file contains no direct write primitive, and matches on the
460
490
  // symbol name even inside a comment.
461
491
  const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
462
- if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
463
- if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
464
- if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
465
- if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
492
+ if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null, undefined, heldWriteLock) });
493
+ if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null, undefined, heldWriteLock) });
494
+ if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null, undefined, heldWriteLock) });
495
+ if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null, undefined, heldWriteLock) });
466
496
  const landed: string[] = [];
467
497
  for (const write of scalarWrites) {
468
498
  try {
@@ -476,6 +506,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
476
506
  return jsonResponse({ error: `writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
477
507
  }
478
508
  }
509
+ } finally {
510
+ if (heldWriteLock) releaseConfigWriteLock(heldWriteLock);
511
+ }
479
512
  // Derived from fresh post-write readers (readConfigText is uncached): upstream
480
513
  // lets an enabled multi_agent_v2 feature override [agents].enabled = false, so
481
514
  // warn rather than reject — silently accepting would imply multi-agent is off.
@@ -527,35 +560,59 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
527
560
  }
528
561
  const body = parsedBody as { enabled?: unknown };
529
562
  if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
530
- const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
563
+ const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, activeCodexConfigPath } = await import("../../codex/features");
531
564
  const before = isDefaultModeRequestUserInputEnabled();
532
565
  let toggle = deps.toggleDefaultModeRequestUserInput;
533
566
  if (!toggle) {
534
- const { runCodexFeaturesCommand } = await import("../../cli/v2");
535
- toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable", DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY);
536
- }
537
- let toggleError: string | null = null;
538
- try {
539
- toggle(body.enabled);
540
- } catch (error) {
541
- const err = error as { stderr?: unknown; message?: string };
542
- const raw = err.stderr;
543
- const stderrText = typeof raw === "string"
544
- ? raw.trim()
545
- : raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
546
- toggleError = stderrText || (err.message ?? String(error));
547
- }
548
- const enabled = isDefaultModeRequestUserInputEnabled();
549
- if (toggleError !== null || enabled !== body.enabled) {
550
- const reason = toggleError
551
- ?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
552
- return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
567
+ try {
568
+ const { codexFeaturesInvocation, runCodexFeaturesCommand } = await import("../../cli/v2");
569
+ const action = body.enabled ? "enable" : "disable";
570
+ const invocation = codexFeaturesInvocation(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, process.platform, { requireAvailable: true });
571
+ toggle = (_enabled, env, validate) => runCodexFeaturesCommand(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, env, validate, invocation);
572
+ } catch (error) {
573
+ return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${error instanceof Error ? error.message : String(error)}` }, 502);
574
+ }
553
575
  }
554
- const warnings: string[] = [];
555
- if (enabled !== before) {
556
- warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
576
+ // The `codex features` subprocess rewrites config.toml itself — run it
577
+ // under the shared write lock so it cannot interleave with an opencodex
578
+ // scalar edit or injection mid-write on either side.
579
+ const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage, runConfigWriteChild, ConfigWriteDestinationChanged } = await import("../../codex/config-write-lock");
580
+ const configPath = activeCodexConfigPath();
581
+ // Preserve native home creation only after executable resolution succeeds.
582
+ prepareCodexHome(dirname(configPath));
583
+ {
584
+ const configLock = await acquireConfigWriteLock(configPath);
585
+ if (!configLock.ok) {
586
+ return jsonResponse({ error: configWriteLockFailureMessage(configLock), retryable: configLock.error === "locked" }, 502);
587
+ }
588
+ let toggleError: string | null = null;
589
+ let destinationChanged = false;
590
+ try {
591
+ runConfigWriteChild(configPath, configLock.handle, (env, validate) => toggle!(body.enabled as boolean, env, validate));
592
+ } catch (error) {
593
+ destinationChanged = error instanceof ConfigWriteDestinationChanged;
594
+ const err = error as { stderr?: unknown; message?: string };
595
+ const raw = err.stderr;
596
+ const stderrText = typeof raw === "string"
597
+ ? raw.trim()
598
+ : raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
599
+ toggleError = stderrText || (err.message ?? String(error));
600
+ } finally {
601
+ releaseConfigWriteLock(configLock.handle);
602
+ }
603
+ if (destinationChanged) return jsonResponse({ error: toggleError, retryable: false }, 502);
604
+ const enabled = isDefaultModeRequestUserInputEnabled();
605
+ if (toggleError !== null || enabled !== body.enabled) {
606
+ const reason = toggleError
607
+ ?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
608
+ return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
609
+ }
610
+ const warnings: string[] = [];
611
+ if (enabled !== before) {
612
+ warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
613
+ }
614
+ return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
557
615
  }
558
- return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
559
616
  }
560
617
 
561
618
  // Subagent prompt injection model: single native or routed model whose info is
@@ -1368,6 +1425,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1368
1425
  );
1369
1426
  const webSearchOverride = config.claudeCode?.webSearchSidecar;
1370
1427
  const visionOverride = config.claudeCode?.visionSidecar;
1428
+ const helperParent = config.claudeCode?.model?.includes("/") ? config.claudeCode.model.split("/")[0] : undefined;
1429
+ const helperVision = { ...config.visionSidecar, ...visionOverride };
1430
+ // Same pool the runtime picks (vision/plan.ts preferredVisionBackend): explicit, else inherited
1431
+ // from the Claude model's instance, so a usable inherited Pool 2 reports the Anthropic backend.
1432
+ const helperVisionInstance = helperVision.anthropicInstance ?? inheritedAnthropicInstance(config, helperParent);
1433
+ const helperVisionBackend = helperVision.backend ?? (sidecarOptionsAuth(config, helperVisionInstance).isAnthropicAuth ? "anthropic" : "openai");
1371
1434
  const { firstPartyDesired, readFirstPartyProxyStatus } = await import("../../claude/first-party-settings");
1372
1435
  const { observeClaudeDesktopMode } = await import("../../claude/desktop-first-party");
1373
1436
  const { claudeInterceptEnabled, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
@@ -1409,13 +1472,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1409
1472
  alwaysEnableEffort: config.claudeCode?.alwaysEnableEffort === true,
1410
1473
  autoContext: config.claudeCode?.autoContext !== false,
1411
1474
  autoCompactWindow: config.claudeCode?.autoCompactWindow ?? null,
1475
+ contextAccounting: config.claudeCode?.contextAccounting === "200k" ? "200k" : "1m",
1412
1476
  blockedSkills: config.claudeCode?.blockedSkills ?? null,
1413
1477
  injectAgents: config.claudeCode?.injectAgents !== false,
1478
+ sidecarPools: {
1479
+ webSearchSidecar: sidecarAnthropicPoolOptions(config, { ...config.webSearchSidecar, ...webSearchOverride }, helperParent),
1480
+ visionSidecar: sidecarAnthropicPoolOptions(config, { ...helperVision, backend: helperVisionBackend }, helperParent),
1481
+ },
1414
1482
  ...(webSearchOverride && Object.keys(webSearchOverride).length > 0
1415
- ? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model } }
1483
+ ? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model, ...(webSearchOverride.anthropicInstance ? { anthropicInstance: webSearchOverride.anthropicInstance } : {}) } }
1416
1484
  : {}),
1417
1485
  ...(visionOverride && Object.keys(visionOverride).length > 0
1418
- ? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model } }
1486
+ ? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model, ...(visionOverride.anthropicInstance ? { anthropicInstance: visionOverride.anthropicInstance } : {}) } }
1419
1487
  : {}),
1420
1488
  fastMode: config.fastMode,
1421
1489
  forceAvailable: [...visibleNativeSlugs(config), ...filterCatalogVisibleModels(models, config).map(catalogModelSlug)],
@@ -1441,7 +1509,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1441
1509
  return prototype === Object.prototype || prototype === null;
1442
1510
  };
1443
1511
  if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
1444
- const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1512
+ const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; contextAccounting?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1445
1513
  if (body.cliFirstParty !== undefined && typeof body.cliFirstParty !== "boolean")
1446
1514
  return jsonResponse({ error: "cliFirstParty must be a boolean" }, 400);
1447
1515
  if (body.cliFirstParty !== undefined && Object.keys(body).length !== 1)
@@ -1563,6 +1631,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1563
1631
  ...(residual ? ["settings_residual"] : []),
1564
1632
  ] });
1565
1633
  }
1634
+ const instanceError = anthropicSidecarPatchError(config, body, true);
1635
+ if (instanceError) return jsonResponse({ error: instanceError }, 400);
1566
1636
  for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
1567
1637
  const section = body[field];
1568
1638
  if (section === undefined || section === null) continue;
@@ -1587,7 +1657,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1587
1657
  // gates cannot drift.
1588
1658
  if (field === "visionSidecar" && typeof section.model === "string" && section.model !== "") {
1589
1659
  const requested = section.model;
1590
- const candidates = await visionCandidateRows(config);
1660
+ const candidates = await visionCandidateRows({ ...config, visionSidecar: {
1661
+ ...config.visionSidecar, ...sidecarSettingsAfterPatch(config.claudeCode?.visionSidecar, section),
1662
+ } });
1591
1663
  const hint = section.backend === "anthropic" || section.backend === "openai"
1592
1664
  || section.backend === "routed"
1593
1665
  ? section.backend
@@ -1610,7 +1682,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1610
1682
  // module as /api/sidecar-settings — a gate on one route and a stale copy on
1611
1683
  // the other is no gate at all.
1612
1684
  if (field === "webSearchSidecar"
1613
- && (section.model !== undefined || section.backend !== undefined)) {
1685
+ && (section.model !== undefined || section.backend !== undefined || section.anthropicInstance !== undefined)) {
1614
1686
  const stored = config.claudeCode?.webSearchSidecar;
1615
1687
  // Validate against the SUBMITTED backend across the whole union, not
1616
1688
  // just openai/anthropic (#2457). allowedBackends above already refused
@@ -1629,7 +1701,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1629
1701
  : typeof section.model === "string"
1630
1702
  ? section.model
1631
1703
  : stored?.model ?? config.webSearchSidecar?.model;
1632
- const candidates = await webSearchCandidateRows(config);
1704
+ const candidates = await webSearchCandidateRows({ ...config, webSearchSidecar: {
1705
+ ...config.webSearchSidecar, ...sidecarSettingsAfterPatch(config.claudeCode?.webSearchSidecar, section),
1706
+ } });
1633
1707
  if (effectiveModel && webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
1634
1708
  return jsonResponse(webSearchModelRejection(
1635
1709
  "webSearchSidecar.model",
@@ -1650,8 +1724,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1650
1724
  }
1651
1725
  // The per-field validation above guarantees vision only ever carries the two-member
1652
1726
  // union; the cast is the loop's shared-shape compromise, not a wider write path.
1653
- const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string };
1727
+ const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string; anthropicInstance?: AnthropicInstanceId | null };
1654
1728
  const override = { ...next[field] } as NonNullable<OcxClaudeCodeConfig[typeof field]>;
1729
+ if (requested.anthropicInstance === null) delete override.anthropicInstance;
1730
+ else if (isAnthropicInstanceId(requested.anthropicInstance)) override.anthropicInstance = requested.anthropicInstance;
1655
1731
  if (requested.backend === null) delete override.backend;
1656
1732
  else if (requested.backend !== undefined) override.backend = requested.backend as never;
1657
1733
  if (requested.model === "") delete override.model;
@@ -1703,6 +1779,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1703
1779
  if (body.autoContext) delete next.autoContext;
1704
1780
  else next.autoContext = false;
1705
1781
  }
1782
+ if (body.contextAccounting !== undefined) {
1783
+ // Sparse like autoContext (devlog/_plan/261009_claude_1m_default/030): "1m" is the default
1784
+ // and drops the key; "200k" is the only stored value.
1785
+ if (body.contextAccounting !== "1m" && body.contextAccounting !== "200k") {
1786
+ return jsonResponse({ error: "contextAccounting must be \"1m\" or \"200k\"" }, 400);
1787
+ }
1788
+ if (body.contextAccounting === "200k") next.contextAccounting = "200k";
1789
+ else delete next.contextAccounting;
1790
+ }
1706
1791
  if (body.injectAgents !== undefined) {
1707
1792
  // Default-on boolean (devlog 260712 070): true = drop the key, false = store.
1708
1793
  if (typeof body.injectAgents !== "boolean") return jsonResponse({ error: "injectAgents must be a boolean" }, 400);
@@ -1817,7 +1902,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1817
1902
  // levers feed the same injection, so a changed or cleared slot must not linger in
1818
1903
  // launchd until the next restart.
1819
1904
  const systemEnvInputs = ["systemEnv", "authMode", "model", "smallFastModel", "tierModels",
1820
- "maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow"] as const;
1905
+ "maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow", "contextAccounting"] as const;
1821
1906
  if (systemEnvInputs.some(field => body[field] !== undefined)) {
1822
1907
  try {
1823
1908
  await applySystemEnvToggle(config, config.port);
@@ -1,7 +1,8 @@
1
+ import { isAnthropicOAuthInstance, configuredAnthropicInstance } from "../../providers/anthropic-instance";
1
2
  import type { OcxConfig } from "../../types";
2
- import { effectiveAnthropicAccountThreshold, parseAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
3
+ import { effectiveAnthropicAccountThresholdForInstance, parseAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
3
4
  import { OAUTH_PROVIDERS } from "../../oauth";
4
- import { setAnthropicAccountThreshold } from "../../oauth/store";
5
+ import { setAnthropicAccountThresholdForInstance } from "../../oauth/store";
5
6
  import { jsonResponse } from "../auth-cors";
6
7
  import { readManagementJsonBodyOr } from "./body";
7
8
 
@@ -10,18 +11,20 @@ export async function handleAnthropicAccountThreshold(req: Request, config: OcxC
10
11
  const body = await readManagementJsonBodyOr(req, {});
11
12
  if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "body must be an object" }, 400);
12
13
  const fields = body as Record<string, unknown>;
13
- const definition = OAUTH_PROVIDERS.anthropic;
14
- const effectiveProvider = config.providers.anthropic
14
+ const provider = fields.provider;
15
+ if (typeof provider !== "string" || !isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
16
+ const definition = OAUTH_PROVIDERS[provider];
17
+ const effectiveProvider = config.providers[provider]
15
18
  ?? definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
16
- if (fields.provider !== "anthropic" || effectiveProvider?.authMode !== "oauth") {
19
+ if (effectiveProvider?.authMode !== "oauth" || (provider === "anthropic2" && !configuredAnthropicInstance(config, provider))) {
17
20
  return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
18
21
  }
19
22
  if (typeof fields.accountId !== "string" || !fields.accountId.trim()) return jsonResponse({ error: "missing accountId" }, 400);
20
23
  const threshold = parseAnthropicAccountThreshold(fields.threshold);
21
24
  if (fields.threshold !== null && threshold === null) return jsonResponse({ error: "threshold must be an integer 0-100 or null" }, 400);
22
- if (!await setAnthropicAccountThreshold(fields.accountId, threshold)) return jsonResponse({ error: "account not found" }, 404);
23
- return jsonResponse({ ok: true, provider: "anthropic", accountId: fields.accountId,
25
+ if (!await setAnthropicAccountThresholdForInstance(provider, fields.accountId, threshold)) return jsonResponse({ error: "account not found" }, 404);
26
+ return jsonResponse({ ok: true, provider, accountId: fields.accountId,
24
27
  autoSwitchThresholdOverride: threshold,
25
- effectiveAutoSwitchThreshold: effectiveAnthropicAccountThreshold(config, { autoSwitchThresholdOverride: threshold ?? undefined }),
26
- autoSwitchThreshold: effectiveAnthropicAccountThreshold(config) });
28
+ effectiveAutoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider, config, { autoSwitchThresholdOverride: threshold ?? undefined }),
29
+ autoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider, config) });
27
30
  }
@@ -0,0 +1,72 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import type { AnthropicAccountPoolConfig } from "../../types/anthropic-account-pool";
3
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../../providers/anthropic-instance";
4
+ import { rawAnthropicAccountPool, resolveAnthropicAccountPoolConfig } from "../../oauth/anthropic-pool-config";
5
+ import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
6
+ import { ConfigWritePublishedError } from "../../config/persist-unlocked";
7
+ import { mutatePersistedConfig } from "../../config";
8
+ import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
9
+ import { jsonResponse } from "../auth-cors";
10
+
11
+ /** Required instance; never synthesizes a deleted B provider row. */
12
+ export function writeAnthropicPoolSettings(config: OcxConfig, instance: AnthropicInstanceId, value: AnthropicAccountPoolConfig): void {
13
+ if (instance === "anthropic") config.anthropicAccountPool = value;
14
+ else {
15
+ const row = config.providers[instance];
16
+ if (!configuredAnthropicInstance(config, instance)) throw new Error("Anthropic pool instance is no longer configured");
17
+ row!.anthropicAccountPool = value;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Both Anthropic writers share publication-aware recovery from the atomic mutation owner.
23
+ * Recovery covers only the durable write. Once the file holds the new value, a stale live row
24
+ * or a failed reconcile is reported as saved with the fixed bookkeeping warning, never a 500.
25
+ */
26
+ export function persistAnthropicPoolPatch(
27
+ config: OcxConfig,
28
+ instance: AnthropicInstanceId,
29
+ patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
30
+ ): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
31
+ const before = readConfigFileSnapshot();
32
+ const unknown = () => ({ status: "failed" as const, response: jsonResponse({
33
+ error: "Pool settings save state is unknown; reload settings before editing again",
34
+ code: "config_save_state_unknown",
35
+ }, 409) });
36
+ const publish = (value: AnthropicAccountPoolConfig, warned: boolean): { status: "saved"; warning?: "config_bookkeeping_failed" } => {
37
+ let warning = warned;
38
+ try { writeAnthropicPoolSettings(config, instance, value); } catch { warning = true; }
39
+ try { reconcileLiveStateStores(); } catch { warning = true; }
40
+ return warning ? { status: "saved", warning: "config_bookkeeping_failed" } : { status: "saved" };
41
+ };
42
+ let durable: AnthropicAccountPoolConfig | undefined;
43
+ try {
44
+ const saved = mutatePersistedConfig(target => {
45
+ if (instance === "anthropic2" && !configuredAnthropicInstance(target, instance)) {
46
+ throw new Error("Anthropic pool instance is no longer configured");
47
+ }
48
+ return patch(target);
49
+ });
50
+ if (saved.status === "unavailable") return unknown();
51
+ durable = saved.value;
52
+ } catch (error) {
53
+ const persisted = readConfigFileSnapshot();
54
+ if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
55
+ // File reads may salvage hand edits; recovery success requires a strict, authoritative document.
56
+ let validated: ReturnType<typeof validateConfigCandidate>;
57
+ try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
58
+ catch { return unknown(); }
59
+ if (!validated.ok) return unknown();
60
+ const current = validated.config;
61
+ if (instance === "anthropic2" && !configuredAnthropicInstance(current, instance)) return unknown();
62
+ let expected: AnthropicAccountPoolConfig;
63
+ try { expected = patch(structuredClone(current)).value; } catch { return unknown(); }
64
+ const matches = JSON.stringify(rawAnthropicAccountPool(current, instance)) === JSON.stringify(expected);
65
+ // A failed live write or reconcile must not turn confirmed publication back into a rollback.
66
+ if (matches) return publish(resolveAnthropicAccountPoolConfig(current, instance), true);
67
+ if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
68
+ return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
69
+ }
70
+ if (durable === undefined) return unknown();
71
+ return publish(durable, false);
72
+ }