@iislee/opencodex 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Opt-in Anthropic OAuth account pool (#294).
3
+ *
4
+ * Default OFF. When enabled:
5
+ * - Sticky session affinity across requests that share a session key
6
+ * - 429 cools the failed account and fails over to another eligible account
7
+ * - New sessions use `strategy` (default quota): lowest known fiveHour usage (#493),
8
+ * round-robin, or fill-first — affinity still wins for bound sessions
9
+ *
10
+ * Intentionally narrower than the Codex pool: no mid-session quota rotation,
11
+ * soft-avoid ladders, or probe leases. Anthropic OAuth is ToS-sensitive.
12
+ *
13
+ * Affinity is process-local (lost on restart). Cooldown uses Retry-After when present,
14
+ * otherwise a default backoff. 401/403 credential failures should set needsReauth on the
15
+ * store (existing OAuth path) so the account is excluded from eligibility.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { setActiveAccount, getAccountSet, getAccountCredential } from "./store";
19
+ import { getCachedProviderAccountQuota } from "../providers/quota";
20
+ import { fallbackCodexAccountLogLabel } from "../codex/account-label";
21
+ import {
22
+ normalizeAccountPoolStickyLimit,
23
+ normalizeAccountPoolStrategy,
24
+ notePoolRotationFailure,
25
+ notePoolRotationSuccess,
26
+ pickRoundRobinAccount,
27
+ POOL_KEY_ANTHROPIC,
28
+ seedPoolRotationAccount,
29
+ } from "../codex/pool-rotation";
30
+ import type { OcxAccountPoolRotationStrategy, OcxConfig } from "../types";
31
+ import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
32
+ import { retainedUtf8Bytes } from "../lib/admission";
33
+
34
+ const PROVIDER = "anthropic";
35
+ const DEFAULT_COOLDOWN_MS = 60_000;
36
+ const MAX_COOLDOWN_MS = 15 * 60_000;
37
+ const AFFINITY_IDLE_TTL_MS = 24 * 60 * 60_000;
38
+ const MAX_AFFINITY_ENTRIES = 2_000;
39
+ const MAX_AFFINITY_COMPONENT_BYTES = 512;
40
+ const UNKNOWN_USAGE_SCORE = 100;
41
+ const DEFAULT_AUTO_SWITCH_THRESHOLD = 80;
42
+ /** Cap same-request 429 rotations so short Retry-After cannot infinite-loop. */
43
+ export const ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST = 3;
44
+
45
+ export interface AnthropicAccountPoolConfig {
46
+ enabled?: boolean;
47
+ /** Usage % for new-session pick. Default 80. 0 = disable quota-based pick (active / affinity only). */
48
+ autoSwitchThreshold?: number;
49
+ /** New-session rotation strategy. Default quota (today's behaviour). */
50
+ strategy?: OcxAccountPoolRotationStrategy;
51
+ /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
52
+ stickyLimit?: number;
53
+ }
54
+
55
+ interface AccountHealth {
56
+ cooldownUntil: number;
57
+ cooldownSource: "retry-after" | "default";
58
+ }
59
+
60
+ interface AffinityEntry {
61
+ accountId: string;
62
+ lastUsedAt: number;
63
+ }
64
+
65
+ const upstreamHealth = new Map<string, AccountHealth>();
66
+ const sessionAffinity = new Map<string, AffinityEntry>();
67
+
68
+ function normalizeAffinityComponent(value: string | null | undefined): string {
69
+ const normalized = value?.trim() ?? "";
70
+ return normalized && retainedUtf8Bytes(normalized) <= MAX_AFFINITY_COMPONENT_BYTES ? normalized : "";
71
+ }
72
+
73
+ export function anthropicAccountPoolConfig(config: OcxConfig): AnthropicAccountPoolConfig {
74
+ const raw = config.anthropicAccountPool;
75
+ if (!raw || typeof raw !== "object") return {};
76
+ return raw;
77
+ }
78
+
79
+ export function isAnthropicAccountPoolEnabled(config: OcxConfig): boolean {
80
+ return anthropicAccountPoolConfig(config).enabled === true;
81
+ }
82
+
83
+ export function anthropicAutoSwitchThreshold(config: OcxConfig): number {
84
+ const value = anthropicAccountPoolConfig(config).autoSwitchThreshold;
85
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100) return value;
86
+ return DEFAULT_AUTO_SWITCH_THRESHOLD;
87
+ }
88
+
89
+ function parseRetryAfterMs(value: string | null | undefined, now: number): number | undefined {
90
+ const text = value?.trim();
91
+ if (!text) return undefined;
92
+ if (/^\d+(?:\.\d+)?$/.test(text)) {
93
+ const seconds = Number(text);
94
+ if (Number.isFinite(seconds) && seconds > 0) {
95
+ return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
96
+ }
97
+ }
98
+ const timestamp = Date.parse(text);
99
+ if (!Number.isFinite(timestamp)) return undefined;
100
+ const delay = timestamp - now;
101
+ return delay > 0 ? Math.min(delay, MAX_COOLDOWN_MS) : undefined;
102
+ }
103
+
104
+ export function getAnthropicAccountHealthSnapshot(
105
+ accountId: string,
106
+ now = Date.now(),
107
+ ): { cooldownUntil?: number; cooldownSource?: AccountHealth["cooldownSource"] } | null {
108
+ const entry = upstreamHealth.get(accountId);
109
+ if (!entry) return null;
110
+ if (entry.cooldownUntil <= now) {
111
+ upstreamHealth.delete(accountId);
112
+ return null;
113
+ }
114
+ return { cooldownUntil: entry.cooldownUntil, cooldownSource: entry.cooldownSource };
115
+ }
116
+
117
+ export function clearAnthropicAccountCooldown(accountId: string): boolean {
118
+ return upstreamHealth.delete(accountId);
119
+ }
120
+
121
+ export function sweepExpiredAnthropicRoutingHealth(now = Date.now()): number {
122
+ let removed = 0;
123
+ for (const [accountId, health] of upstreamHealth) {
124
+ if (health.cooldownUntil > now) continue;
125
+ upstreamHealth.delete(accountId);
126
+ removed += 1;
127
+ }
128
+ return removed;
129
+ }
130
+
131
+ /** Test / logout helper. */
132
+ export function clearAnthropicAccountPoolState(): void {
133
+ upstreamHealth.clear();
134
+ sessionAffinity.clear();
135
+ }
136
+
137
+ export function anthropicSessionAffinitySizeForTests(): number {
138
+ return sessionAffinity.size;
139
+ }
140
+
141
+ function isCooled(accountId: string, now: number): boolean {
142
+ return getAnthropicAccountHealthSnapshot(accountId, now) !== null;
143
+ }
144
+
145
+ function hasKnownUsage(accountId: string): boolean {
146
+ const quota = getCachedProviderAccountQuota(PROVIDER, accountId);
147
+ return typeof quota?.fiveHourPercent === "number" && Number.isFinite(quota.fiveHourPercent);
148
+ }
149
+
150
+ function usageScore(accountId: string): number {
151
+ const quota = getCachedProviderAccountQuota(PROVIDER, accountId);
152
+ if (!quota || typeof quota.fiveHourPercent !== "number" || !Number.isFinite(quota.fiveHourPercent)) {
153
+ return UNKNOWN_USAGE_SCORE;
154
+ }
155
+ return Math.max(0, Math.min(100, quota.fiveHourPercent));
156
+ }
157
+
158
+ const TOKEN_SKEW_MS = 60_000;
159
+
160
+ /** Background `local-cli` slots with expired access are not pool-eligible (identity adoption risk). */
161
+ function isPoolCredentialUsable(accountId: string, now: number): boolean {
162
+ const cred = getAccountCredential(PROVIDER, accountId);
163
+ if (!cred) return false;
164
+ if (cred.source !== "local-cli") return true;
165
+ if (canRefreshAnthropicPoolAccount(accountId)) return true;
166
+ return cred.expires > now + TOKEN_SKEW_MS;
167
+ }
168
+
169
+ export function getEligibleAnthropicAccounts(now = Date.now()): string[] {
170
+ const set = getAccountSet(PROVIDER);
171
+ if (!set) return [];
172
+ return set.accounts
173
+ .filter(account =>
174
+ account.needsReauth !== true
175
+ && !isCooled(account.id, now)
176
+ && isPoolCredentialUsable(account.id, now))
177
+ .map(account => account.id);
178
+ }
179
+
180
+ /** Earliest remaining cooldown among cooled Anthropic accounts, for client Retry-After. */
181
+ export function getAnthropicPoolRetryAfterSeconds(now = Date.now()): number | null {
182
+ const set = getAccountSet(PROVIDER);
183
+ if (!set) return null;
184
+ let earliest: number | null = null;
185
+ for (const account of set.accounts) {
186
+ const snap = getAnthropicAccountHealthSnapshot(account.id, now);
187
+ if (!snap?.cooldownUntil) continue;
188
+ if (earliest === null || snap.cooldownUntil < earliest) earliest = snap.cooldownUntil;
189
+ }
190
+ if (earliest === null || earliest <= now) return null;
191
+ return Math.max(1, Math.ceil((earliest - now) / 1000));
192
+ }
193
+
194
+ function pickLowestUsage(excludeId: string | undefined, now: number): string | null {
195
+ const eligible = getEligibleAnthropicAccounts(now).filter(id => id !== excludeId);
196
+ if (eligible.length === 0) return null;
197
+ let best = eligible[0]!;
198
+ let bestScore = usageScore(best);
199
+ for (let i = 1; i < eligible.length; i++) {
200
+ const id = eligible[i]!;
201
+ const score = usageScore(id);
202
+ if (score < bestScore) {
203
+ best = id;
204
+ bestScore = score;
205
+ }
206
+ }
207
+ return best;
208
+ }
209
+
210
+ /** Next eligible Anthropic account in stable order after `afterId` (wrapping). */
211
+ function pickNextFillFirstAnthropicAccount(
212
+ config: OcxConfig,
213
+ afterId: string,
214
+ eligible: string[],
215
+ ): string | null {
216
+ if (eligible.length === 0) return null;
217
+ const ordered = [...eligible].sort((a, b) => a.localeCompare(b));
218
+ const set = getAccountSet(PROVIDER);
219
+ const stableAll = set
220
+ ? [...set.accounts.map(a => a.id)].sort((a, b) => a.localeCompare(b))
221
+ : ordered;
222
+ const startIdx = stableAll.indexOf(afterId);
223
+ if (startIdx < 0) {
224
+ for (const id of ordered) {
225
+ if (isActiveUnderFillFirstThreshold(config, id)) return id;
226
+ }
227
+ return ordered[0] ?? null;
228
+ }
229
+ // Skip successors that are also at/above threshold (known drained usage).
230
+ let fallback: string | null = null;
231
+ for (let step = 1; step <= stableAll.length; step++) {
232
+ const candidate = stableAll[(startIdx + step) % stableAll.length]!;
233
+ if (!eligible.includes(candidate)) continue;
234
+ if (!fallback) fallback = candidate;
235
+ if (isActiveUnderFillFirstThreshold(config, candidate)) return candidate;
236
+ }
237
+ return fallback ?? ordered[0] ?? null;
238
+ }
239
+
240
+ function pickAlternateAnthropicAccount(
241
+ config: OcxConfig,
242
+ excludeId: string,
243
+ now: number,
244
+ ): string | null {
245
+ const strategy = anthropicPoolStrategy(config);
246
+ const eligible = getEligibleAnthropicAccounts(now).filter(id => id !== excludeId);
247
+ if (strategy === "round-robin") {
248
+ return pickRoundRobinAccount(POOL_KEY_ANTHROPIC, eligible, stickyLimitForPool(config));
249
+ }
250
+ if (strategy === "fill-first") {
251
+ return pickNextFillFirstAnthropicAccount(config, excludeId, eligible);
252
+ }
253
+ return pickLowestUsage(excludeId, now);
254
+ }
255
+
256
+ function pruneExpiredAffinity(now: number): void {
257
+ for (const [key, entry] of sessionAffinity) {
258
+ if (now - entry.lastUsedAt > AFFINITY_IDLE_TTL_MS) sessionAffinity.delete(key);
259
+ }
260
+ if (sessionAffinity.size <= MAX_AFFINITY_ENTRIES) return;
261
+ const sorted = [...sessionAffinity.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
262
+ const drop = sessionAffinity.size - MAX_AFFINITY_ENTRIES;
263
+ for (let i = 0; i < drop; i++) sessionAffinity.delete(sorted[i]![0]);
264
+ }
265
+
266
+ export type AnthropicAccountSelectionReason =
267
+ | "pool-disabled"
268
+ | "affinity"
269
+ | "active"
270
+ | "lowest-usage"
271
+ | "only-eligible"
272
+ | "round-robin"
273
+ | "fill-first"
274
+ | "none"
275
+ | "all-cooled";
276
+
277
+ export interface AnthropicAccountSelection {
278
+ accountId: string | null;
279
+ reason: AnthropicAccountSelectionReason;
280
+ }
281
+
282
+ function stickyLimitForPool(config: OcxConfig): number {
283
+ return normalizeAccountPoolStickyLimit(anthropicAccountPoolConfig(config).stickyLimit);
284
+ }
285
+
286
+ function anthropicPoolStrategy(config: OcxConfig): OcxAccountPoolRotationStrategy {
287
+ return normalizeAccountPoolStrategy(anthropicAccountPoolConfig(config).strategy);
288
+ }
289
+
290
+ function isActiveUnderFillFirstThreshold(config: OcxConfig, accountId: string): boolean {
291
+ const threshold = anthropicAutoSwitchThreshold(config);
292
+ if (threshold <= 0) return true;
293
+ // Unknown usage must not force fill-first to abandon the active account.
294
+ if (!hasKnownUsage(accountId)) return true;
295
+ return usageScore(accountId) < threshold;
296
+ }
297
+
298
+ /**
299
+ * Fill-first: keep eligible active under threshold; otherwise advance to the next
300
+ * eligible id in stable sorted order after the current active (wrapping).
301
+ */
302
+ function pickFillFirstAnthropicAccount(config: OcxConfig, now: number): string | null {
303
+ const eligible = getEligibleAnthropicAccounts(now);
304
+ if (eligible.length === 0) return null;
305
+
306
+ const set = getAccountSet(PROVIDER);
307
+ const active = set?.activeAccountId;
308
+ if (active && eligible.includes(active) && isActiveUnderFillFirstThreshold(config, active)) {
309
+ return active;
310
+ }
311
+
312
+ if (!active || !set) {
313
+ const ordered = [...eligible].sort((a, b) => a.localeCompare(b));
314
+ for (const id of ordered) {
315
+ if (isActiveUnderFillFirstThreshold(config, id)) return id;
316
+ }
317
+ return ordered[0] ?? null;
318
+ }
319
+
320
+ return pickNextFillFirstAnthropicAccount(config, active, eligible);
321
+ }
322
+
323
+ /**
324
+ * Unbound new-session pick for round-robin / fill-first. Returns null to fall through
325
+ * to the legacy quota path (or when the strategy is quota).
326
+ */
327
+ function pickUnboundStrategyAccount(
328
+ config: OcxConfig,
329
+ now: number,
330
+ ): { accountId: string; reason: "round-robin" | "fill-first" } | null {
331
+ const strategy = anthropicPoolStrategy(config);
332
+ if (strategy === "quota") return null;
333
+
334
+ if (strategy === "round-robin") {
335
+ const eligible = getEligibleAnthropicAccounts(now);
336
+ const limit = stickyLimitForPool(config);
337
+ const picked = pickRoundRobinAccount(POOL_KEY_ANTHROPIC, eligible, limit);
338
+ if (!picked) return null;
339
+ notePoolRotationSuccess(POOL_KEY_ANTHROPIC, picked, limit);
340
+ return { accountId: picked, reason: "round-robin" };
341
+ }
342
+
343
+ if (strategy === "fill-first") {
344
+ const picked = pickFillFirstAnthropicAccount(config, now);
345
+ if (!picked) return null;
346
+ return { accountId: picked, reason: "fill-first" };
347
+ }
348
+
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * Resolve which Anthropic OAuth account should serve this session.
354
+ * When the pool is disabled, always returns the store's active account.
355
+ */
356
+ export function resolveAnthropicAccountForSession(
357
+ sessionKey: string | null | undefined,
358
+ config: OcxConfig,
359
+ now = Date.now(),
360
+ ): AnthropicAccountSelection {
361
+ pruneExpiredAffinity(now);
362
+ const set = getAccountSet(PROVIDER);
363
+ if (!set || set.accounts.length === 0) return { accountId: null, reason: "none" };
364
+
365
+ if (!isAnthropicAccountPoolEnabled(config)) {
366
+ return { accountId: set.activeAccountId, reason: "pool-disabled" };
367
+ }
368
+
369
+ const key = normalizeAffinityComponent(sessionKey);
370
+ if (key) {
371
+ const affined = sessionAffinity.get(key);
372
+ if (affined && now - affined.lastUsedAt <= AFFINITY_IDLE_TTL_MS) {
373
+ const stillThere = set.accounts.some(a => a.id === affined.accountId && a.needsReauth !== true);
374
+ if (stillThere && !isCooled(affined.accountId, now) && isPoolCredentialUsable(affined.accountId, now)) {
375
+ affined.lastUsedAt = now;
376
+ return { accountId: affined.accountId, reason: "affinity" };
377
+ }
378
+ sessionAffinity.delete(key);
379
+ }
380
+ }
381
+
382
+ const strategy = anthropicPoolStrategy(config);
383
+ // No session identity (Desktop turns without a sticky key): hold the current
384
+ // active under RR/fill-first instead of treating every turn as a new session.
385
+ // Round-robin only when there is a real new-session key (or active is unusable).
386
+ if (!key && (strategy === "round-robin" || strategy === "fill-first")) {
387
+ const activeOk = set.accounts.some(a => a.id === set.activeAccountId && a.needsReauth !== true)
388
+ && !isCooled(set.activeAccountId, now)
389
+ && isPoolCredentialUsable(set.activeAccountId, now);
390
+ if (activeOk) {
391
+ return { accountId: set.activeAccountId, reason: "active" };
392
+ }
393
+ }
394
+
395
+ const strategyPick = pickUnboundStrategyAccount(config, now);
396
+ if (strategyPick) {
397
+ // Do not promote active here — token validation may still fail. Callers
398
+ // (responses/core) promote after getAnthropicPoolAccessToken succeeds.
399
+ if (key && normalizeAffinityComponent(strategyPick.accountId)) {
400
+ sessionAffinity.set(key, { accountId: strategyPick.accountId, lastUsedAt: now });
401
+ pruneExpiredAffinity(now);
402
+ }
403
+ return { accountId: strategyPick.accountId, reason: strategyPick.reason };
404
+ }
405
+
406
+ const threshold = anthropicAutoSwitchThreshold(config);
407
+ const activeOk = set.accounts.some(a => a.id === set.activeAccountId && a.needsReauth !== true)
408
+ && !isCooled(set.activeAccountId, now)
409
+ && isPoolCredentialUsable(set.activeAccountId, now);
410
+
411
+ let accountId: string | null = null;
412
+ let reason: AnthropicAccountSelectionReason = "none";
413
+
414
+ if (threshold > 0) {
415
+ // Unknown usage must NOT force a switch away from the healthy active account.
416
+ if (activeOk && (!hasKnownUsage(set.activeAccountId) || usageScore(set.activeAccountId) < threshold)) {
417
+ accountId = set.activeAccountId;
418
+ reason = "active";
419
+ } else {
420
+ const picked = pickLowestUsage(undefined, now);
421
+ if (picked) {
422
+ accountId = picked;
423
+ reason = activeOk && picked === set.activeAccountId ? "active" : "lowest-usage";
424
+ } else if (activeOk) {
425
+ accountId = set.activeAccountId;
426
+ reason = "active";
427
+ }
428
+ }
429
+ } else if (activeOk) {
430
+ accountId = set.activeAccountId;
431
+ reason = "active";
432
+ } else {
433
+ const picked = pickLowestUsage(set.activeAccountId, now);
434
+ if (picked) {
435
+ accountId = picked;
436
+ reason = "only-eligible";
437
+ }
438
+ }
439
+
440
+ if (!accountId) {
441
+ const anyCooled = set.accounts.some(a => isCooled(a.id, now));
442
+ return { accountId: null, reason: anyCooled ? "all-cooled" : "none" };
443
+ }
444
+
445
+ if (key && normalizeAffinityComponent(accountId)) {
446
+ sessionAffinity.set(key, { accountId, lastUsedAt: now });
447
+ pruneExpiredAffinity(now);
448
+ }
449
+ return { accountId, reason };
450
+ }
451
+
452
+ export function bindAnthropicSessionAffinity(
453
+ sessionKey: string | null | undefined,
454
+ accountId: string,
455
+ now = Date.now(),
456
+ ): void {
457
+ const key = normalizeAffinityComponent(sessionKey);
458
+ if (!key || !normalizeAffinityComponent(accountId)) return;
459
+ sessionAffinity.set(key, { accountId, lastUsedAt: now });
460
+ pruneExpiredAffinity(now);
461
+ }
462
+
463
+ export function clearAnthropicSessionAffinityForAccount(accountId: string): void {
464
+ for (const [key, entry] of sessionAffinity) {
465
+ if (entry.accountId === accountId) sessionAffinity.delete(key);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Record a 429 for `failedAccountId`, cool it, clear its affinity, and pick a failover
471
+ * account. Does NOT promote the store active account — caller should promote only after a
472
+ * successful retry (or token resolve).
473
+ */
474
+ export function rotateAnthropicAccountOn429(
475
+ config: OcxConfig,
476
+ failedAccountId: string,
477
+ retryAfterHeader: string | null | undefined,
478
+ sessionKey?: string | null,
479
+ now = Date.now(),
480
+ ): string | null {
481
+ if (!isAnthropicAccountPoolEnabled(config)) return null;
482
+
483
+ const parsedRetry = parseRetryAfterMs(retryAfterHeader, now);
484
+ const cooldownMs = parsedRetry ?? DEFAULT_COOLDOWN_MS;
485
+ upstreamHealth.set(failedAccountId, {
486
+ cooldownUntil: now + cooldownMs,
487
+ cooldownSource: parsedRetry ? "retry-after" : "default",
488
+ });
489
+ sweepExpiredOnWrite(now);
490
+ clearAnthropicSessionAffinityForAccount(failedAccountId);
491
+ notePoolRotationFailure(POOL_KEY_ANTHROPIC, failedAccountId);
492
+
493
+ const next = pickAlternateAnthropicAccount(config, failedAccountId, now);
494
+ if (!next) {
495
+ console.warn("[anthropic-pool] all eligible Anthropic OAuth accounts are in cooldown; returning 429");
496
+ return null;
497
+ }
498
+
499
+ const affinityKey = normalizeAffinityComponent(sessionKey);
500
+ if (affinityKey && normalizeAffinityComponent(next)) {
501
+ sessionAffinity.set(affinityKey, { accountId: next, lastUsedAt: now });
502
+ pruneExpiredAffinity(now);
503
+ }
504
+ console.warn(
505
+ `[anthropic-pool] 429 on ${formatAnthropicAccountOrdinal(failedAccountId)}; failing over to ${formatAnthropicAccountOrdinal(next)}`,
506
+ );
507
+ return next;
508
+ }
509
+
510
+ /** Promote dashboard active account after a validated failover target is usable. */
511
+ export function promoteAnthropicActiveAccount(accountId: string): void {
512
+ void setActiveAccount(PROVIDER, accountId).catch(() => { /* best-effort */ });
513
+ }
514
+
515
+ /**
516
+ * Manual selection resets session affinity and seeds the RR ring so the next
517
+ * unbound new session honors the operator-chosen account (Codex parity).
518
+ */
519
+ export function resetAnthropicRoutingForManualSelection(accountId: string): void {
520
+ sessionAffinity.clear();
521
+ seedPoolRotationAccount(POOL_KEY_ANTHROPIC, accountId);
522
+ }
523
+
524
+ /**
525
+ * Resolve a bearer for pool traffic without adopting a newer global Claude CLI
526
+ * credential into a background multiauth `local-cli` slot (same fail-closed rule
527
+ * as quota probes).
528
+ */
529
+ export async function getAnthropicPoolAccessToken(accountId: string): Promise<string> {
530
+ const stored = getAccountCredential(PROVIDER, accountId);
531
+ if (!stored) {
532
+ const { OAuthLoginRequiredError } = await import("./index");
533
+ throw new OAuthLoginRequiredError(PROVIDER);
534
+ }
535
+ if (stored.expires > Date.now() + TOKEN_SKEW_MS) return stored.access;
536
+ if (!canRefreshAnthropicPoolAccount(accountId)) {
537
+ throw new Error("background local-cli token expired; refuse CLI-adopting refresh for pool");
538
+ }
539
+ const { getValidAccessTokenForAccount } = await import("./index");
540
+ return getValidAccessTokenForAccount(PROVIDER, accountId);
541
+ }
542
+
543
+ /**
544
+ * Whether the pool may refresh this account's token. Background `local-cli` slots must not
545
+ * adopt the global Claude CLI credential (same fail-closed rule as quota probes).
546
+ */
547
+ export function canRefreshAnthropicPoolAccount(accountId: string): boolean {
548
+ const set = getAccountSet(PROVIDER);
549
+ const cred = getAccountCredential(PROVIDER, accountId);
550
+ if (!cred) return false;
551
+ if (cred.source !== "local-cli") return true;
552
+ return set?.activeAccountId === accountId;
553
+ }
554
+
555
+ export function formatAnthropicAccountOrdinal(accountId: string): string {
556
+ return fallbackCodexAccountLogLabel(accountId);
557
+ }
558
+
559
+ export function formatAnthropicProviderForLog(
560
+ providerName: string,
561
+ accountId: string | null | undefined,
562
+ _config?: OcxConfig,
563
+ ): string {
564
+ if (!accountId) return providerName;
565
+ return `${providerName}-${formatAnthropicAccountOrdinal(accountId)}`;
566
+ }
567
+
568
+ /**
569
+ * Build a sticky session key from Claude/Responses headers.
570
+ * Prefer true session/thread ids; do not use Desktop shared cache-cohort prompt_cache_key
571
+ * alone (those collide across conversations).
572
+ */
573
+ export function anthropicSessionKeyFromParts(input: {
574
+ sessionIdHeader?: string | null;
575
+ threadIdHeader?: string | null;
576
+ promptCacheKey?: string | null;
577
+ clientThreadId?: string | null;
578
+ /** When true, prompt_cache_key is a shared Desktop cohort — ignore it for affinity. */
579
+ promptCacheKeyIsSharedCohort?: boolean;
580
+ }): string | null {
581
+ const preferred = (
582
+ input.clientThreadId
583
+ ?? input.sessionIdHeader
584
+ ?? input.threadIdHeader
585
+ ?? ""
586
+ ).trim();
587
+ if (preferred) {
588
+ return preferred.length <= 128 ? preferred : createHash("sha256").update(preferred).digest("hex");
589
+ }
590
+ if (input.promptCacheKeyIsSharedCohort) return null;
591
+ const cacheKey = input.promptCacheKey?.trim() ?? "";
592
+ if (!cacheKey) return null;
593
+ return cacheKey.length <= 128 ? cacheKey : createHash("sha256").update(cacheKey).digest("hex");
594
+ }