@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
@@ -1,3 +1,4 @@
1
+ import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
1
2
  import type { OcxAccountPoolRotationStrategy } from "../types";
2
3
  import type { GenerationContext } from "../lib/state-store-sweeper";
3
4
 
@@ -277,7 +278,7 @@ export function clearPoolRotationState(poolKey?: string): void {
277
278
 
278
279
  export function reconcilePoolRotationState(context: GenerationContext): number {
279
280
  if (context.generation <= lastReconciledGeneration) return 0;
280
- const anthropicIds = new Set<string>();
281
+ const anthropicIds = new Map<AnthropicInstanceId, Set<string>>();
281
282
  // Live account ids per generic OAuth provider, built from the same
282
283
  // `provider\0id` roster the Anthropic pass already walks. Without this the
283
284
  // `generic:*` entries fall through as unknown and are never swept, so a removed
@@ -288,8 +289,10 @@ export function reconcilePoolRotationState(context: GenerationContext): number {
288
289
  if (separator <= 0) continue;
289
290
  const provider = key.slice(0, separator);
290
291
  const accountId = key.slice(separator + 1);
291
- if (provider === "anthropic") {
292
- anthropicIds.add(accountId);
292
+ if (isAnthropicInstanceId(provider)) {
293
+ let ids = anthropicIds.get(provider);
294
+ if (!ids) { ids = new Set(); anthropicIds.set(provider, ids); }
295
+ ids.add(accountId);
293
296
  continue;
294
297
  }
295
298
  let bucket = genericIds.get(provider);
@@ -301,8 +304,8 @@ export function reconcilePoolRotationState(context: GenerationContext): number {
301
304
  }
302
305
  let removed = 0;
303
306
  for (const [poolKey, state] of selectionState) {
304
- const valid = poolKey === POOL_KEY_ANTHROPIC
305
- ? anthropicIds
307
+ const valid = isAnthropicInstanceId(poolKey)
308
+ ? anthropicIds.get(poolKey) ?? new Set<string>()
306
309
  : poolKey === POOL_KEY_CODEX || poolKey.startsWith(`${POOL_KEY_CODEX}:`)
307
310
  ? context.codexAccountIds
308
311
  : poolKey.startsWith("generic:")
@@ -328,3 +331,6 @@ export function reconcilePoolRotationState(context: GenerationContext): number {
328
331
  lastReconciledGeneration = context.generation;
329
332
  return removed;
330
333
  }
334
+
335
+ /** Separate cursor and weights for each Anthropic OAuth instance. */
336
+ export function anthropicPoolKey(instance: AnthropicInstanceId): string { return instance; }
@@ -1,3 +1,5 @@
1
+ import { isBuiltinAnthropicInstanceRow, isAnthropicInstanceId } from "../providers/anthropic-instance";
2
+ import { resolveAnthropicAccountPoolConfig } from "./anthropic-pool-config";
1
3
  import { isGenericFailoverProvider } from "./generic-account-failover";
2
4
  import { parseAccountPoolStickyLimit, parseAccountPoolStrategy, parseCodexAccountPoolStrategy } from "./pool-kernel";
3
5
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -25,8 +27,8 @@ export type GenericPoolStrategy = typeof GENERIC_POOL_STRATEGIES[number];
25
27
 
26
28
  export function poolSettingsCapability(name: string, provider: OcxProviderConfig | undefined): PoolSettingsKind | null {
27
29
  if (name === "openai") return "codex";
28
- if (name === "anthropic") return "anthropic";
29
- if (!provider) return null;
30
+ if (name === "anthropic" || (provider?.disabled !== true && isBuiltinAnthropicInstanceRow(name, provider))) return "anthropic";
31
+ if (name === "anthropic2" || !provider) return null;
30
32
  return isGenericFailoverProvider(name, provider) ? "generic" : null;
31
33
  }
32
34
 
@@ -173,7 +175,7 @@ export function unifiedPoolSettingsDto(
173
175
  };
174
176
  }
175
177
  if (kind === "anthropic") {
176
- const pool = config.anthropicAccountPool ?? {};
178
+ const pool = isAnthropicInstanceId(provider) ? resolveAnthropicAccountPoolConfig(config, provider) : {};
177
179
  const enabled = typeof pool.enabled === "boolean" ? pool.enabled : null;
178
180
  return {
179
181
  ...base,
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { getConfigPath } from "../config/paths";
4
+ import { anthropicInstanceRowShapeMatches, type AnthropicInstanceId, type AnthropicInstanceRow } from "../providers/anthropic-instance-id";
5
+ import type { OcxConfig } from "../types";
6
+ import { normalizeAnthropicIdentity } from "./anthropic-identity";
7
+ import type { AuthStore } from "./store";
8
+ import type { OAuthCredentials } from "./types";
9
+
10
+ export class AnthropicCrossInstanceDuplicateError extends Error {
11
+ readonly code = "ANTHROPIC_CROSS_INSTANCE_DUPLICATE";
12
+ constructor() {
13
+ super("This Anthropic credential is already registered in the other pool; use a different account or remove the existing registration first.");
14
+ this.name = "AnthropicCrossInstanceDuplicateError";
15
+ }
16
+ }
17
+
18
+ export class AnthropicLocalCliImportError extends Error {
19
+ readonly code = "ANTHROPIC_LOCAL_CLI_IMPORT_FORBIDDEN";
20
+ constructor() {
21
+ super("Anthropic Pool 2 supports browser OAuth only; local Claude Code credentials cannot be imported or adopted.");
22
+ this.name = "AnthropicLocalCliImportError";
23
+ }
24
+ }
25
+
26
+ export class AnthropicInstanceCollisionError extends Error {
27
+ readonly code = "ANTHROPIC_INSTANCE_COLLISION";
28
+ constructor(credentialWritten = false) {
29
+ super("anthropic2 is already configured as a custom provider; rename the custom provider before logging in to Pool 2."
30
+ + (credentialWritten ? " The saved Pool 2 credential remains an orphan auth row; remove it or rename the custom provider before retrying." : ""));
31
+ this.name = "AnthropicInstanceCollisionError";
32
+ }
33
+ }
34
+
35
+ export function assertAnthropicInstanceLoginConfig(config: Pick<OcxConfig, "providers">, provider: string, credentialWritten = false): void {
36
+ if (provider !== "anthropic2") return;
37
+ const row = config.providers.anthropic2;
38
+ if (Object.hasOwn(config.providers, provider) && !anthropicInstanceRowShapeMatches(provider, row)) {
39
+ throw new AnthropicInstanceCollisionError(credentialWritten);
40
+ }
41
+ // A malformed custom row (for example, missing baseUrl) can disappear behind loadConfig's
42
+ // fallback. Raw presence still owns this namespace; onboarding must not overwrite its bytes.
43
+ let raw: string;
44
+ try { raw = readFileSync(getConfigPath(), "utf8"); }
45
+ catch (error) {
46
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
47
+ throw new Error("Cannot verify Anthropic Pool 2 ownership in the existing config file.");
48
+ }
49
+ let parsed: unknown;
50
+ try { parsed = JSON.parse(raw.replace(/^\uFEFF/, "")); }
51
+ catch { throw new Error("Cannot verify Anthropic Pool 2 ownership in the existing config file."); }
52
+ const providers = parsed && typeof parsed === "object" && "providers" in parsed ? parsed.providers : undefined;
53
+ if (providers && typeof providers === "object" && Object.hasOwn(providers, provider)) {
54
+ const persisted = (providers as Record<string, unknown>)[provider];
55
+ if (!persisted || typeof persisted !== "object" || Array.isArray(persisted)
56
+ || !anthropicInstanceRowShapeMatches(provider, persisted as AnthropicInstanceRow)) {
57
+ throw new AnthropicInstanceCollisionError(credentialWritten);
58
+ }
59
+ }
60
+ }
61
+
62
+ export function assertAnthropicCredentialSource(instance: AnthropicInstanceId, credential: OAuthCredentials): void {
63
+ if (instance === "anthropic2" && credential.source === "local-cli") throw new AnthropicLocalCliImportError();
64
+ }
65
+
66
+ function tokenFingerprint(token: string): string | undefined {
67
+ return token.length > 0 ? createHash("sha256").update(token).digest("hex") : undefined;
68
+ }
69
+
70
+ /** Registration only, called under the auth-store writer lock before any row changes. */
71
+ export function assertNoCrossAnthropicRegistration(store: AuthStore, instance: AnthropicInstanceId, credential: OAuthCredentials): void {
72
+ assertAnthropicCredentialSource(instance, credential);
73
+ const other = instance === "anthropic" ? "anthropic2" : "anthropic";
74
+ const tokens = new Set([tokenFingerprint(credential.access), tokenFingerprint(credential.refresh)]
75
+ .filter((fingerprint): fingerprint is string => fingerprint !== undefined));
76
+ const identity = normalizeAnthropicIdentity(credential.anthropicIdentity, credential.access);
77
+ // Paused, needs-reauth and orphan rows retain ownership; labels are never proof.
78
+ for (const row of store[other]?.accounts ?? []) {
79
+ const access = tokenFingerprint(row.credential.access);
80
+ const refresh = tokenFingerprint(row.credential.refresh);
81
+ const existingIdentity = normalizeAnthropicIdentity(row.credential.anthropicIdentity, row.credential.access);
82
+ if ((access !== undefined && tokens.has(access)) || (refresh !== undefined && tokens.has(refresh))
83
+ || (identity && existingIdentity && identity.accountUuid === existingIdentity.accountUuid)) {
84
+ throw new AnthropicCrossInstanceDuplicateError();
85
+ }
86
+ }
87
+ }
@@ -20,10 +20,12 @@
20
20
  * both append distinct identified accounts under multiauth.
21
21
  */
22
22
  import { normalizeAnthropicIdentity, matchesAnthropicImport, mergeAnthropicIdentity } from "./anthropic-identity";
23
+ import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
24
+ import { assertNoCrossAnthropicRegistration, assertAnthropicCredentialSource, assertAnthropicInstanceLoginConfig } from "./store-anthropic-instance";
23
25
  import { createHash, randomUUID } from "node:crypto";
24
26
  import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
25
27
  import { join } from "node:path";
26
- import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret, withConfigMutationLockSync } from "../config";
28
+ import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret, withConfigMutationLockSync, loadConfig } from "../config";
27
29
  import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
28
30
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
29
31
  import { recordOwnedConfigPath } from "../lib/config-ownership";
@@ -630,6 +632,9 @@ function normalizeAccount(value: unknown): ProviderAccount | null {
630
632
  const account: ProviderAccount = { id: candidate.id, credential };
631
633
  if (typeof candidate.alias === "string" && candidate.alias.trim()) account.alias = candidate.alias.trim();
632
634
  if (candidate.needsReauth === true) account.needsReauth = true;
635
+ if (candidate.refreshAttentionGeneration === credentialGeneration(credential)) {
636
+ account.refreshAttentionGeneration = candidate.refreshAttentionGeneration;
637
+ }
633
638
  if (candidate.paused === true) account.paused = true;
634
639
  const threshold = parseAnthropicAccountThreshold(candidate.autoSwitchThresholdOverride);
635
640
  if (threshold !== null) account.autoSwitchThresholdOverride = threshold;
@@ -862,14 +867,26 @@ export interface OAuthCredentialWriteReceipt {
862
867
  previousAccount: ProviderAccount | undefined;
863
868
  }
864
869
 
870
+ /** Recheck B immediately before persist, including writes queued behind another mutation. */
871
+ function registrationPersistGuard(provider: string, assertBeforePersist?: () => void): (() => void) | undefined {
872
+ if (provider !== "anthropic2") return assertBeforePersist;
873
+ return () => {
874
+ assertBeforePersist?.();
875
+ assertAnthropicInstanceLoginConfig(loadConfig(), provider);
876
+ };
877
+ }
878
+
865
879
  export async function saveCredentialWithReceipt(
866
880
  provider: string,
867
881
  cred: OAuthCredentials,
868
882
  opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
869
883
  ): Promise<OAuthCredentialWriteReceipt | null> {
884
+ if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
870
885
  const safe = normalizeCredential(cred);
871
886
  if (!safe) return null;
872
887
  const receipt = await mutateStore(store => {
888
+ if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
889
+ if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
873
890
  const set = store[provider];
874
891
  const previousActiveAccountId = set?.activeAccountId;
875
892
  const previousSelectionRevision = set?.selectionRevision;
@@ -892,6 +909,7 @@ export async function saveCredentialWithReceipt(
892
909
  set.selectionRevision = previousSelectionRevision;
893
910
  delete existing.needsReauth;
894
911
  delete existing.needsReauthReason;
912
+ delete existing.refreshAttentionGeneration;
895
913
  accountId = existing.id;
896
914
  } else {
897
915
  const id = distinctAccountId(safe, set.accounts);
@@ -905,6 +923,7 @@ export async function saveCredentialWithReceipt(
905
923
  existing.credential = safe;
906
924
  delete existing.needsReauth;
907
925
  delete existing.needsReauthReason;
926
+ delete existing.refreshAttentionGeneration;
908
927
  if (existing.paused !== true) set.activeAccountId = existing.id;
909
928
  accountId = existing.id;
910
929
  } else {
@@ -917,6 +936,7 @@ export async function saveCredentialWithReceipt(
917
936
  active.credential = safe;
918
937
  delete active.needsReauth;
919
938
  delete active.needsReauthReason;
939
+ delete active.refreshAttentionGeneration;
920
940
  accountId = active.id;
921
941
  } else {
922
942
  const id = distinctAccountId(safe, set.accounts);
@@ -937,6 +957,7 @@ export async function saveCredentialWithReceipt(
937
957
  active.credential = safe;
938
958
  delete active.needsReauth;
939
959
  delete active.needsReauthReason;
960
+ delete active.refreshAttentionGeneration;
940
961
  accountId = active.id;
941
962
  } else {
942
963
  const id = distinctAccountId(safe, set.accounts);
@@ -965,7 +986,7 @@ export async function saveCredentialWithReceipt(
965
986
  previousAccount: previousAccounts.get(accountId),
966
987
  };
967
988
  }, [provider, safe], {
968
- assertBeforePersist: opts.assertBeforePersist,
989
+ assertBeforePersist: registrationPersistGuard(provider, opts.assertBeforePersist),
969
990
  finalizeResult: (receipt, store) => {
970
991
  receipt.selectionRevision = store[provider]?.selectionRevision;
971
992
  },
@@ -1064,11 +1085,14 @@ export async function upsertCredentialByIdentity(
1064
1085
  provider: string,
1065
1086
  cred: OAuthCredentials,
1066
1087
  ): Promise<"inserted" | "updated"> {
1088
+ if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
1067
1089
  const safe = normalizeCredential(cred);
1068
1090
  if (!safe || (!safe.accountId && !safe.email)) {
1069
1091
  throw new Error("Refusing to persist OAuth credential without verified identity");
1070
1092
  }
1071
1093
  return await mutateStore(store => {
1094
+ if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
1095
+ if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
1072
1096
  const set = store[provider];
1073
1097
  const matchesEmailOnly = (account: ProviderAccount): boolean => {
1074
1098
  if (account.credential.accountId) return false;
@@ -1086,6 +1110,7 @@ export async function upsertCredentialByIdentity(
1086
1110
  existing.credential = safe;
1087
1111
  delete existing.needsReauth;
1088
1112
  delete existing.needsReauthReason;
1113
+ delete existing.refreshAttentionGeneration;
1089
1114
  set.activeAccountId ??= existing.id;
1090
1115
  return "updated";
1091
1116
  }
@@ -1098,7 +1123,7 @@ export async function upsertCredentialByIdentity(
1098
1123
  store[provider] = { activeAccountId: id, accounts: [account] };
1099
1124
  }
1100
1125
  return "inserted";
1101
- }, [provider, safe]);
1126
+ }, [provider, safe], { assertBeforePersist: registrationPersistGuard(provider) });
1102
1127
  }
1103
1128
 
1104
1129
  /**
@@ -1172,16 +1197,19 @@ export function getAccountCredentialWithStatus(
1172
1197
  };
1173
1198
  }
1174
1199
 
1175
- /** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
1200
+ /** Persist an explicit login/reauth credential for one account; runtime rotations use mergeAccountCredential. */
1176
1201
  export async function saveAccountCredential(
1177
1202
  provider: string,
1178
1203
  accountId: string,
1179
1204
  cred: OAuthCredentials,
1180
1205
  opts: { assertBeforePersist?: () => void; rotateLoginId?: boolean } = {},
1181
1206
  ): Promise<void> {
1207
+ if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
1182
1208
  const safe = normalizeCredential(cred);
1183
1209
  if (!safe) return;
1184
1210
  await mutateStore(store => {
1211
+ if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
1212
+ if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
1185
1213
  const set = store[provider];
1186
1214
  const account = set?.accounts.find(a => a.id === accountId);
1187
1215
  if (!set || !account) return;
@@ -1189,6 +1217,7 @@ export async function saveAccountCredential(
1189
1217
  if (opts.rotateLoginId) account.loginId = randomUUID();
1190
1218
  delete account.needsReauth;
1191
1219
  delete account.needsReauthReason;
1220
+ delete account.refreshAttentionGeneration;
1192
1221
  // A pause that found no usable fallback leaves the active id on a paused row. Once this
1193
1222
  // reauthentication makes an unpaused account usable, hand the selection to it.
1194
1223
  const active = set.accounts.find(a => a.id === set.activeAccountId);
@@ -1196,7 +1225,7 @@ export async function saveAccountCredential(
1196
1225
  set.activeAccountId = account.id;
1197
1226
  set.selectionRevision = randomUUID();
1198
1227
  }
1199
- }, [provider, accountId, safe], { assertBeforePersist: opts.assertBeforePersist });
1228
+ }, [provider, accountId, safe], { assertBeforePersist: registrationPersistGuard(provider, opts.assertBeforePersist) });
1200
1229
  }
1201
1230
 
1202
1231
  function accountSelection(set: ProviderAccountSet): OAuthAccountSelection {
@@ -1275,13 +1304,20 @@ export async function setAnthropicAccountThreshold(
1275
1304
  accountId: string,
1276
1305
  threshold: number | null,
1277
1306
  options: { assertBeforePersist?: () => void } = {},
1307
+ ): Promise<boolean> {
1308
+ return setAnthropicAccountThresholdForInstance("anthropic", accountId, threshold, options);
1309
+ }
1310
+
1311
+ export async function setAnthropicAccountThresholdForInstance(
1312
+ instance: AnthropicInstanceId, accountId: string, threshold: number | null,
1313
+ options: { assertBeforePersist?: () => void } = {},
1278
1314
  ): Promise<boolean> {
1279
1315
  const normalizedThreshold = threshold === null ? null : parseAnthropicAccountThreshold(threshold);
1280
1316
  if (threshold !== null && normalizedThreshold === null) {
1281
1317
  throw new Error("threshold must be an integer 0-100 or null");
1282
1318
  }
1283
1319
  const result = await mutateStore(store => {
1284
- const set = store.anthropic;
1320
+ const set = store[instance];
1285
1321
  const account = set?.accounts.find(row => row.id === accountId);
1286
1322
  if (!set || !account) return { status: "not-found" as const };
1287
1323
  if ((account.autoSwitchThresholdOverride ?? null) === normalizedThreshold) return { status: "unchanged" as const };
@@ -1290,12 +1326,12 @@ export async function setAnthropicAccountThreshold(
1290
1326
  else account.autoSwitchThresholdOverride = normalizedThreshold;
1291
1327
  set.selectionRevision = randomUUID();
1292
1328
  return { status: "updated" as const, before, after: accountSelection(set) };
1293
- }, [accountId, normalizedThreshold], { assertBeforePersist: options.assertBeforePersist, afterPersist: result => {
1329
+ }, [instance, accountId, normalizedThreshold], { assertBeforePersist: options.assertBeforePersist, afterPersist: result => {
1294
1330
  if (result.status !== "updated") return;
1295
1331
  // Publish the policy-owned transition before the generic selection event. This
1296
1332
  // preserves an exact previous-revision manual intent without opening an ABA gap.
1297
1333
  publishOAuthAccountRoutingPolicyChange(Object.freeze({
1298
- provider: "anthropic",
1334
+ provider: instance,
1299
1335
  before: Object.freeze(result.before),
1300
1336
  after: Object.freeze(result.after),
1301
1337
  }));
@@ -1403,6 +1439,7 @@ export async function replaceProviderAccountSet(
1403
1439
  credential: { ...account.credential, ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}) },
1404
1440
  ...(account.alias ? { alias: account.alias } : {}),
1405
1441
  ...(account.needsReauth ? { needsReauth: true } : {}),
1442
+ ...(account.refreshAttentionGeneration ? { refreshAttentionGeneration: account.refreshAttentionGeneration } : {}),
1406
1443
  ...(account.paused ? { paused: true } : {}),
1407
1444
  ...(account.autoSwitchThresholdOverride !== undefined ? { autoSwitchThresholdOverride: account.autoSwitchThresholdOverride } : {}),
1408
1445
  ...(account.needsReauthReason === "verify_account" ? { needsReauthReason: account.needsReauthReason } : {}),
@@ -1468,7 +1505,28 @@ export async function markAccountNeedsReauth(
1468
1505
  }, [provider, accountId]);
1469
1506
  }
1470
1507
 
1471
- export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
1508
+ export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");if(isAnthropicInstanceId(provider))assertAnthropicCredentialSource(provider,safe);return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;delete account.refreshAttentionGeneration;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
1472
1509
  // A late refresh failure must not change an operator-paused account's health. Check under
1473
1510
  // the mutation lock, not before awaiting it, so a concurrent pause cannot be overwritten.
1474
- export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account"):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
1511
+ export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account",isCurrent?:(store:AuthStore)=>boolean):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;if(isCurrent&&!isCurrent(store))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
1512
+
1513
+ /** Attention is projected only; refresh selection continues to use the stored terminal flag. */
1514
+ export function accountNeedsReauthForStatus(provider: string, account: ProviderAccount, now = Date.now()): boolean {
1515
+ return account.needsReauth === true || (provider === "kiro" && account.credential.expires <= now
1516
+ && account.refreshAttentionGeneration === credentialGeneration(account.credential));
1517
+ }
1518
+
1519
+ /** Record refresh evidence under the same generation/config/pause fences as terminal health. */
1520
+ export async function markAccountRefreshAttentionIfGeneration(
1521
+ provider: string, accountId: string, generation: string, writerGeneration = captureConfigGeneration(),
1522
+ ): Promise<boolean> {
1523
+ if (provider !== "kiro") return false;
1524
+ const key = oauthAccountKey(provider, accountId);
1525
+ return await mutateStore(store => {
1526
+ const account = store[provider]?.accounts.find(a => a.id === accountId);
1527
+ if (!account || account.paused || credentialGeneration(account.credential) !== generation) return false;
1528
+ if (writerGeneration < lastReconciledGeneration && !liveOAuthAccountKeys.has(key)) return false;
1529
+ account.refreshAttentionGeneration = generation;
1530
+ return true;
1531
+ }, [provider, accountId, generation]);
1532
+ }
@@ -29,6 +29,7 @@ import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
29
29
  import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
30
30
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
31
31
  import { providerCodexAccountMode } from "../providers/registry";
32
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
32
33
  import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
33
34
  import { tryAcquireNativeMainProfileClaim } from "../codex/native-main-admission";
34
35
 
@@ -144,6 +145,7 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
144
145
  // skipping accounts already marked needsReauth (terminal; only a re-login fixes them) or
145
146
  // paused by the operator (manual exclusion from all automatic account use).
146
147
  for (const provider of listOAuthProviders()) {
148
+ if (provider === "anthropic2" && configuredAnthropicInstance(config, provider) === undefined) continue;
147
149
  if (resolveRefreshPolicy(provider, config) !== "proactive") continue;
148
150
  for (const account of listAccounts(provider)) {
149
151
  if (account.needsReauth || account.paused) continue;
@@ -90,6 +90,8 @@ export interface ProviderAccount {
90
90
  credential: OAuthCredentials;
91
91
  /** Terminal refresh failure (invalid_grant / reused / revoked) — re-login required. */
92
92
  needsReauth?: boolean;
93
+ /** Kiro SSO refresh input-error evidence; status-only attention after this generation expires. */
94
+ refreshAttentionGeneration?: string;
93
95
  /** Operator exclusion from generic OAuth account selection until explicitly resumed. */
94
96
  paused?: boolean;
95
97
  /** Anthropic-only usage-switch override; absent inherits its pool default, zero disables it. */
@@ -23,6 +23,7 @@ import { getRoutingProfile, POLICY_NAMESPACE, resolvePolicyProfileId } from "../
23
23
  import { resolveWireProtocolOverride } from "../server/adapter-resolve";
24
24
  import { nativeChatDeclineReason } from "../server/chat-native-eligibility";
25
25
  import { nativeMessagesDeclineReason } from "../server/messages-native-eligibility";
26
+ import { messagesSelectorTargetsSecondaryInstance, messagesSecondaryInstanceUnavailable } from "../server/messages-native-selector";
26
27
  import { parseSyntheticRowId } from "../server/fast-row";
27
28
  import type { OcxConfig } from "../types";
28
29
  import { inboundWireForProtocol, type Protocol, type ProtocolReasonCode } from "./contract";
@@ -161,6 +162,7 @@ function messagesPassthroughPossible(config: OcxConfig, model: string): boolean
161
162
  if (config.claudeCode?.nativePassthrough === false) return false;
162
163
  if (!/^(claude|anthropic)/i.test(model)) return false;
163
164
  try {
165
+ if (messagesSelectorTargetsSecondaryInstance(config, model)) return false;
164
166
  return resolveInboundModel(model, config.claudeCode) === model;
165
167
  } catch {
166
168
  return false;
@@ -204,6 +206,9 @@ export function buildProtocolPlanSnapshot(
204
206
  // A synthetic row needs the proxy-owned adapter, so it never takes the passthrough.
205
207
  if (!syntheticRow && messagesPassthroughPossible(config, request.model)) reasonCodes.push("caller-credential-required");
206
208
  try {
209
+ if (messagesSecondaryInstanceUnavailable(config, routeKey)) {
210
+ return { ...base, routeKind: "unknown", candidates: [], reasonCodes: [...reasonCodes, "unknown-model"] };
211
+ }
207
212
  routeKey = resolveInboundModel(routeKey, config.claudeCode);
208
213
  } catch {
209
214
  return { ...base, routeKind: "unknown", candidates: [], reasonCodes };
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * Nothing else reads those keys directly: endpoint admission, `count_tokens`, the endpoint
5
5
  * metadata DTO and the dashboard all resolve through here, so they cannot disagree about
6
- * whether an API is open. Type-only config import keeps this module free of runtime edges.
6
+ * whether an API is open. Instance admission reads only config and registry metadata;
7
+ * this module never reads credentials or selects an account.
7
8
  */
8
9
  import type { OcxConfig } from "../types";
10
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
9
11
  import type { Protocol } from "./contract";
10
12
 
11
13
  export type ApiSurfaceSource =
@@ -70,8 +72,10 @@ export interface ProtocolSettings {
70
72
  rollout: Readonly<ProtocolRolloutSettings>;
71
73
  }
72
74
 
75
+ type ProtocolConfig = Pick<OcxConfig, "protocols" | "anthropicAccountPool"> & Partial<Pick<OcxConfig, "providers">>;
76
+
73
77
  /** Resolve protocol policy with conservative defaults for every absent or malformed field. */
74
- export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "anthropicAccountPool">, providerName?: string): Readonly<ProtocolSettings> {
78
+ export function resolveProtocolSettings(config: ProtocolConfig, providerName?: string): Readonly<ProtocolSettings> {
75
79
  const raw: unknown = config.protocols;
76
80
  const protocols = isRec(raw) ? raw : {};
77
81
  const rollout = isRec(protocols.rollout) ? protocols.rollout : {};
@@ -79,9 +83,11 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "a
79
83
  // A present malformed container cannot become an absent/default-on policy.
80
84
  const validContainers = (raw === undefined || isRec(raw))
81
85
  && (!Object.hasOwn(protocols, "rollout") || isRec(protocols.rollout));
82
- const rawPool: unknown = config.anthropicAccountPool;
86
+ const instance = configuredAnthropicInstance({ providers: config.providers ?? {} }, providerName);
87
+ const rawPool: unknown = instance === "anthropic2"
88
+ ? config.providers?.anthropic2?.anthropicAccountPool : instance === "anthropic" ? config.anthropicAccountPool : undefined;
83
89
  const pool = isRec(rawPool) ? rawPool : {};
84
- const pooled = providerName === "anthropic" && pool.enabled === true;
90
+ const pooled = instance !== undefined && pool.enabled === true;
85
91
  const poolPreference = !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true;
86
92
  const nativeOn = (key: "managedMessagesNative" | "managedMessagesNativeOAuth") => validContainers
87
93
  && (!pooled || poolPreference)
@@ -105,15 +111,17 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "a
105
111
  * dashboard can tell a preview computed under an older policy from a current one. Not a
106
112
  * security boundary; FNV-1a over a canonical JSON projection.
107
113
  */
108
- export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols" | "anthropicAccountPool">): string {
114
+ export function protocolPolicyRevision(config: ProtocolConfig & Pick<OcxConfig, "apiSurfaces" | "claudeCode">): string {
109
115
  const surfaces = resolveApiSurfaceSettings(config);
110
116
  const settings = resolveProtocolSettings(config);
111
117
  const anthropicSettings = resolveProtocolSettings(config, "anthropic");
118
+ const secondarySettings = resolveProtocolSettings(config, "anthropic2");
112
119
  const state = (record: unknown, key: string) => !isRec(record) || !Object.hasOwn(record, key)
113
120
  ? "absent" : record[key] === true ? "true" : record[key] === false ? "false" : "invalid";
114
121
  const container = (value: unknown) => value === undefined ? "absent" : isRec(value) ? "object" : "invalid";
115
122
  const rawProtocols: unknown = config.protocols;
116
123
  const rawRollout = isRec(rawProtocols) ? rawProtocols.rollout : undefined;
124
+ const secondary = config.providers?.anthropic2;
117
125
  const canonical = JSON.stringify({
118
126
  messages: [surfaces.messages.enabled, surfaces.messages.source],
119
127
  unrepresentable: settings.unrepresentable,
@@ -122,6 +130,11 @@ export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "
122
130
  state(rawRollout, "managedMessagesNative"), state(rawRollout, "managedMessagesNativeOAuth"),
123
131
  container(config.anthropicAccountPool), state(config.anthropicAccountPool, "enabled"),
124
132
  state(config.anthropicAccountPool, "nativeMessages"),
133
+ container(secondary), state(secondary, "disabled"),
134
+ !!secondary && Object.hasOwn(secondary, "anthropicOAuthInstance"), secondary?.anthropicOAuthInstance ?? "absent",
135
+ secondary?.adapter ?? "absent", secondary?.authMode ?? "absent", secondary?.baseUrl ?? "absent",
136
+ container(secondary?.anthropicAccountPool), state(secondary?.anthropicAccountPool, "enabled"),
137
+ state(secondary?.anthropicAccountPool, "nativeMessages"),
125
138
  ],
126
139
  rollout: [
127
140
  settings.rollout.nativeChatCombos,
@@ -129,6 +142,8 @@ export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "
129
142
  settings.rollout.managedMessagesNativeOAuth,
130
143
  anthropicSettings.rollout.managedMessagesNative,
131
144
  anthropicSettings.rollout.managedMessagesNativeOAuth,
145
+ secondarySettings.rollout.managedMessagesNative,
146
+ secondarySettings.rollout.managedMessagesNativeOAuth,
132
147
  settings.rollout.directEncoders,
133
148
  settings.rollout.shadowPlan,
134
149
  ],
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Identity of the two builtin Anthropic OAuth account-pool instances.
3
+ *
4
+ * This module has no imports on purpose: the provider registry and config types depend on it, and
5
+ * `./anthropic-instance.ts` (which reads the registry) builds on top of it. Keep it that way, or the
6
+ * registry and its lookup helper import each other.
7
+ *
8
+ * `anthropic` is the original pool. `anthropic2` is a second, independent pool that runs the same
9
+ * Anthropic implementation with its own credentials, pool settings and runtime state. Exactly these two
10
+ * IDs exist; a prefix such as `anthropic3` is never an instance.
11
+ */
12
+ export const ANTHROPIC_INSTANCE_IDS = ["anthropic", "anthropic2"] as const;
13
+ export type AnthropicInstanceId = typeof ANTHROPIC_INSTANCE_IDS[number];
14
+
15
+ /** The canonical pool. Bare `claude-*` inference, Claude Code caller-forward and CLI import stay here. */
16
+ export const ANTHROPIC_PRIMARY_INSTANCE: AnthropicInstanceId = "anthropic";
17
+ /** The second, independent pool ("Anthropic · Pool 2"). Browser OAuth only. */
18
+ export const ANTHROPIC_POOL2_INSTANCE: AnthropicInstanceId = "anthropic2";
19
+
20
+ export type AnthropicInstanceRow = {
21
+ adapter?: string;
22
+ authMode?: string;
23
+ baseUrl?: string;
24
+ anthropicOAuthInstance?: unknown;
25
+ };
26
+
27
+ export function isAnthropicInstanceId(value: unknown): value is AnthropicInstanceId {
28
+ return value === "anthropic" || value === "anthropic2";
29
+ }
30
+
31
+ /**
32
+ * Whether a configured provider row may be treated as the builtin instance of that name.
33
+ *
34
+ * `anthropic` keeps its historical meaning: the row named `anthropic` is the instance. A row named
35
+ * `anthropic2` may predate the builtin as a user's custom provider, so it is the builtin only when it is
36
+ * explicitly marked as an Anthropic OAuth instance. Transport endpoints never establish ownership.
37
+ * Anything else keeps its custom meaning and is never pinned, enriched or routed as the builtin pool.
38
+ */
39
+ export function anthropicInstanceRowShapeMatches(
40
+ name: string,
41
+ row: AnthropicInstanceRow | undefined,
42
+ ): boolean {
43
+ if (name === "anthropic") return true;
44
+ if (name !== "anthropic2" || !row) return false;
45
+ return Object.hasOwn(row, "anthropicOAuthInstance") && row.anthropicOAuthInstance === "anthropic2"
46
+ && row.adapter === "anthropic" && row.authMode === "oauth";
47
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Registry- and config-aware Anthropic instance checks. See `./anthropic-instance-id.ts` for the IDs and
3
+ * the pure row-shape rule; this module adds the registry declaration and the configured-row lookup.
4
+ *
5
+ * Instance membership is never inferred from `adapter === "anthropic"` (the API-key provider and
6
+ * Anthropic-compatible gateways share that adapter) or from a name prefix.
7
+ */
8
+ import type { OcxConfig, OcxProviderConfig } from "../types";
9
+ import { getProviderRegistryEntry } from "./registry";
10
+ import { anthropicInstanceRowShapeMatches, isAnthropicInstanceId, type AnthropicInstanceId } from "./anthropic-instance-id";
11
+
12
+ export * from "./anthropic-instance-id";
13
+
14
+ /** Exact registry check: one of the two IDs, declared as an Anthropic-family OAuth entry with its own OAuth ID. */
15
+ export function isAnthropicOAuthInstance(id: string | undefined): id is AnthropicInstanceId {
16
+ if (!isAnthropicInstanceId(id)) return false;
17
+ const entry = getProviderRegistryEntry(id);
18
+ return entry !== undefined
19
+ && entry.authKind === "oauth"
20
+ && entry.oauthId === id
21
+ && entry.oauthFamily === "anthropic";
22
+ }
23
+
24
+ export function isBuiltinAnthropicInstanceRow(
25
+ name: string,
26
+ row?: Partial<Pick<OcxProviderConfig, "adapter" | "authMode" | "baseUrl" | "anthropicOAuthInstance">>,
27
+ ): boolean {
28
+ return isAnthropicOAuthInstance(name) && anthropicInstanceRowShapeMatches(name, row);
29
+ }
30
+
31
+ /**
32
+ * The instance a configured provider name runs as, or undefined.
33
+ *
34
+ * For `anthropic` this is a compatibility identity result: it returns the instance whenever the name is
35
+ * `anthropic`, and callers keep every auth-mode, enabled-provider and eligibility check they already
36
+ * apply. For `anthropic2` the row must exist, must not be disabled and must pass the builtin row-shape
37
+ * rule, so an orphan `anthropic2` auth row or a custom provider of that name never activates the pool.
38
+ */
39
+ export function configuredAnthropicInstance(
40
+ config: Pick<OcxConfig, "providers">,
41
+ name: string | undefined,
42
+ ): AnthropicInstanceId | undefined {
43
+ if (!isAnthropicOAuthInstance(name)) return undefined;
44
+ if (name === "anthropic") return "anthropic";
45
+ const row = config.providers?.[name];
46
+ if (!row || row.disabled === true) return undefined;
47
+ return anthropicInstanceRowShapeMatches(name, row) ? name : undefined;
48
+ }