mixdog 1.2.0 → 1.2.2

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 (386) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/scripts/gate-local.mjs +19 -3
  4. package/scripts/model-catalog-audit.mjs +1 -15
  5. package/src/agents/reviewer/AGENT.md +3 -3
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +5 -0
  8. package/src/defaults/skills/pptx/SKILL.md +6 -6
  9. package/src/defaults/skills/pptx/references/review.md +6 -4
  10. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  11. package/src/defaults/skills/setup/references/actions.md +13 -3
  12. package/src/defaults/skills/visualize/SKILL.md +12 -9
  13. package/src/repl/slash-commands.mjs +16 -1
  14. package/src/rules/shared/60-verification.md +3 -1
  15. package/src/rules/shared/70-delivery.md +4 -2
  16. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  17. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  18. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  19. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  20. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  21. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  22. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  23. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +122 -15
  24. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +54 -11
  25. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +104 -9
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +358 -140
  27. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  29. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  30. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  34. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  36. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  37. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +294 -44
  38. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  40. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  42. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  45. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  46. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  50. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  52. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  53. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  54. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  55. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  56. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +53 -18
  57. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  58. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  59. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  60. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  61. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  62. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  63. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  64. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  65. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  66. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +447 -0
  67. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  68. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  70. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  71. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  72. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  73. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  74. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  75. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +86 -19
  76. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  78. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  79. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  80. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  81. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  82. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  83. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  84. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  85. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  86. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  87. package/src/runtime/agent/orchestrator/providers/gemini.mjs +97 -51
  88. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  89. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  90. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  91. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  92. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  94. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  95. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  96. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  97. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  98. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  99. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +5 -3
  100. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  101. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  103. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  109. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  110. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  111. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  112. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  113. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +6 -3
  114. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  115. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +26 -6
  116. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  117. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  119. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  120. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  121. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  123. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  124. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  125. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  126. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  127. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  128. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  129. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  130. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  131. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  132. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  133. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  134. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  135. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +35 -24
  136. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  137. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  138. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  139. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  140. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  141. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  142. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  143. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  145. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  146. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  147. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  148. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  149. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  150. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +268 -181
  151. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  152. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  153. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  154. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  155. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +270 -0
  156. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  157. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  158. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  160. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  161. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  162. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  163. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  164. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  165. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  166. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  167. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  168. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  169. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -4
  170. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  171. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  172. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  173. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  174. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +42 -5
  175. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +14 -3
  176. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  177. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +50 -23
  178. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  179. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +22 -14
  180. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  181. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  182. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  183. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  184. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  185. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  186. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  187. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  188. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  189. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  190. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +819 -0
  191. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +12 -0
  192. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +22 -12
  193. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  194. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  195. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  196. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  197. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +41 -3
  198. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  199. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  200. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  201. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  202. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  203. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  204. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  205. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  206. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  207. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  208. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  209. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  210. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  211. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  212. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  213. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  214. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  215. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  216. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  217. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  218. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  219. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  220. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  221. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  222. package/src/runtime/attachments/store.d.mts +3 -0
  223. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  224. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  225. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  226. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  227. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  228. package/src/runtime/memory/lib/memory.mjs +1 -1
  229. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  230. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  231. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  232. package/src/runtime/office/README.md +1 -1
  233. package/src/runtime/office/shared/values.mjs +0 -5
  234. package/src/runtime/shared/atomic-file.mjs +25 -4
  235. package/src/runtime/shared/background-tasks.mjs +92 -14
  236. package/src/runtime/shared/err-text.mjs +25 -0
  237. package/src/runtime/shared/error-presentation.mjs +159 -19
  238. package/src/runtime/shared/extension-scopes.mjs +5 -3
  239. package/src/runtime/shared/file-lock.mjs +37 -11
  240. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  241. package/src/runtime/shared/llm/anthropic-effort.mjs +134 -131
  242. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  243. package/src/runtime/shared/llm/model-catalog.mjs +53 -52
  244. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  245. package/src/runtime/shared/llm/provider-catalog-registry.mjs +17 -0
  246. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  247. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  248. package/src/runtime/shared/model-display.mjs +13 -0
  249. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  250. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  251. package/src/runtime/shared/pristine-execution.mjs +28 -4
  252. package/src/runtime/shared/provider-accounts.mjs +124 -20
  253. package/src/runtime/shared/provider-api-key.mjs +12 -0
  254. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  255. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  256. package/src/runtime/shared/shell-display.mjs +9 -3
  257. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  258. package/src/runtime/shared/skill-mentions.mjs +133 -0
  259. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  260. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  261. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  262. package/src/runtime/shared/tool-primitives.mjs +35 -2
  263. package/src/runtime/shared/turn-changes.mjs +447 -219
  264. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  265. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  266. package/src/runtime/shared/turn-worktree-snapshot.mjs +160 -51
  267. package/src/runtime/shared/worker-requests.mjs +15 -2
  268. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  269. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  270. package/src/session-runtime/automation-agents.mjs +8 -2
  271. package/src/session-runtime/boot/apis.mjs +61 -0
  272. package/src/session-runtime/boot/config.mjs +5 -2
  273. package/src/session-runtime/boot/facade.mjs +1 -0
  274. package/src/session-runtime/boot/infrastructure.mjs +35 -18
  275. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  276. package/src/session-runtime/conversation-import/service.mjs +369 -0
  277. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  278. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  279. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  280. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  281. package/src/session-runtime/mcp-glue/connect.mjs +83 -16
  282. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  283. package/src/session-runtime/mcp-glue/server-catalog.mjs +135 -33
  284. package/src/session-runtime/mcp-glue.mjs +25 -2
  285. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  286. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  287. package/src/session-runtime/model-route/set-route.mjs +83 -15
  288. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  289. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  290. package/src/session-runtime/new-session-config.mjs +3 -2
  291. package/src/session-runtime/notification-bus.mjs +9 -2
  292. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  293. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  294. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  295. package/src/session-runtime/provider-readiness.mjs +16 -10
  296. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  297. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  298. package/src/session-runtime/resource-extension-sync.mjs +24 -5
  299. package/src/session-runtime/resource-mcp-api.mjs +198 -78
  300. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  301. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  302. package/src/session-runtime/resource-skills-api.mjs +23 -2
  303. package/src/session-runtime/runtime-review-api.mjs +13 -0
  304. package/src/session-runtime/schedule-session-run.mjs +1 -1
  305. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  306. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  307. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  308. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  310. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  311. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  312. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  313. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  314. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  315. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  316. package/src/session-runtime/services/provider-admin.mjs +64 -18
  317. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  318. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  319. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  320. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  321. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  322. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  323. package/src/session-runtime/skills-api.mjs +7 -3
  324. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  325. package/src/session-runtime/webhook-session-run.mjs +16 -4
  326. package/src/skill-script-entry.mjs +36 -0
  327. package/src/standalone/daemon-drain-policy.mjs +8 -0
  328. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  329. package/src/standalone/daemon-telemetry.mjs +55 -0
  330. package/src/standalone/daemon.mjs +44 -11
  331. package/src/standalone/session-protocol.mjs +12 -0
  332. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  333. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  334. package/src/standalone/session-service/agent-tree.mjs +12 -0
  335. package/src/standalone/session-service/quota-resume.mjs +8 -0
  336. package/src/standalone/session-service/retention/busy.mjs +15 -1
  337. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  338. package/src/standalone/session-service-api.mjs +5 -0
  339. package/src/standalone/session-service.mjs +1 -0
  340. package/src/tui/App.jsx +9 -1
  341. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  342. package/src/tui/app/app-view.jsx +7 -0
  343. package/src/tui/app/create-app-pickers.mjs +4 -0
  344. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  345. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  346. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  347. package/src/tui/app/extension-pickers.mjs +2 -1
  348. package/src/tui/app/model-options.mjs +13 -16
  349. package/src/tui/app/skill-mention-palette.mjs +88 -0
  350. package/src/tui/app/slash-commands.mjs +7 -2
  351. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  352. package/src/tui/app/slash-palette.mjs +23 -7
  353. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  354. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  355. package/src/tui/app/use-prompt-state.mjs +5 -0
  356. package/src/tui/components/Message.jsx +22 -1
  357. package/src/tui/components/PromptInput.jsx +21 -3
  358. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  359. package/src/tui/components/StatusLine.jsx +2 -0
  360. package/src/tui/components/TranscriptItem.jsx +9 -1
  361. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  362. package/src/tui/components/status-line/local-line.mjs +5 -2
  363. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  364. package/src/tui/components/user-message-skills.mjs +38 -0
  365. package/src/tui/dist/index.mjs +1323 -214
  366. package/src/tui/session/queue-helpers.mjs +7 -2
  367. package/src/tui/session/quota-resume.mjs +46 -14
  368. package/src/tui/session/session-api/commands.mjs +33 -24
  369. package/src/tui/session/session-api/extensions.mjs +26 -2
  370. package/src/tui/session/session-api/integrations.mjs +7 -0
  371. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  372. package/src/tui/session/session-api/routes.mjs +71 -7
  373. package/src/tui/session/session-api/settings.mjs +12 -14
  374. package/src/tui/session/session-flow/drain.mjs +21 -5
  375. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  376. package/src/tui/session/tui-steering-persist.mjs +25 -9
  377. package/src/tui/session/turn.mjs +55 -15
  378. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  379. package/src/ui/statusline-agents.mjs +15 -4
  380. package/src/ui/statusline.mjs +7 -3
  381. package/src/ui/streaming-markdown-heal.mjs +34 -3
  382. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  383. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  384. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
  385. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  386. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -3,23 +3,68 @@ import {
3
3
  accountScopedSessionId,
4
4
  readProviderAccountPool,
5
5
  chooseProviderAccount,
6
- changeProviderAccounts,
6
+ selectProviderAccountIf,
7
7
  blockProviderAccount,
8
8
  providerAccountExhausted,
9
+ providerAccountExhaustedForModelOnly,
10
+ providerAccountPolicy,
9
11
  providerAccountsResetAt,
10
12
  } from '../../../shared/provider-accounts.mjs';
11
13
  import { withProviderAccount, hasExplicitProviderAuthBinding } from '../../../shared/provider-auth-binding.mjs';
12
- import { retryAfterMsFromError, isExplicitUserAbortError, typedErrorCode } from './retry-classifier.mjs';
14
+ import { isExplicitUserAbortError, markProviderRecoveryExhausted } from './retry-classifier.mjs';
15
+ import { sleepWithAbort } from './retry-backoff.mjs';
16
+ import { QUOTA_KINDS, providerErrorKind } from './provider-error-kind.mjs';
13
17
  import { fetchOAuthUsageSnapshot, readCachedOAuthUsageSnapshot } from './oauth-usage.mjs';
14
18
 
15
- // Only an explicit quota refusal advances the roster; a generic 429, auth
16
- // refusal, policy denial or transport failure retains its existing recovery.
19
+ // Only an explicit quota / plan / billing refusal advances the roster as
20
+ // quota; a generic 429, overload, auth refusal, policy denial or transport
21
+ // failure retains its existing recovery.
17
22
  export function isAccountQuotaError(error) {
18
- const status = Number(error?.status || error?.httpStatus || error?.response?.status || 0);
19
- if (status === 401 || status === 403 || isExplicitUserAbortError(error)) return false;
20
- return ['insufficient_quota', 'quota_exceeded', 'usage_limit_reached', 'usage_not_included'].includes(
21
- String(typedErrorCode(error) || error?.error?.type || '').toLowerCase()
22
- );
23
+ if (isExplicitUserAbortError(error)) return false;
24
+ const { kind, status } = providerErrorKind(error);
25
+ if (status === 401 || status === 403) return false;
26
+ return QUOTA_KINDS.has(kind);
27
+ }
28
+
29
+ // Kinds that say nothing about the account: the server (or the network) is
30
+ // temporarily unable to serve (or has no capacity in the requested service
31
+ // tier). They never block or rotate an account.
32
+ const SERVER_SIDE_KINDS = new Set(['overloaded', 'transient', 'service_tier_unavailable']);
33
+
34
+ // Provider-supplied roster cooldowns (`accountCooldownPolicy` on the provider
35
+ // object), used when the server named no delay:
36
+ // { floorMs, rateLimitedMs, quotaStepsMs, unknownMs, rotateOnRateLimit,
37
+ // waitForCooldownMs, blockedMs }
38
+ // blockedMs: cooldown for an `accountBlocked` refusal, which then rotates.
39
+ // quotaStepsMs escalates per consecutive quota refusal of one account (reset
40
+ // by a served answer). Absent → the inferred five minutes.
41
+ // rotateOnRateLimit rotates on a plain 429 rate limit; its advice, when
42
+ // present, becomes that account's cooldown.
43
+ // waitForCooldownMs: when every eligible account is only cooling down (no
44
+ // quota evidence), the send waits — cancellable, reported as reconnect
45
+ // progress — for the earliest cooldown while the send's total wait stays
46
+ // within this bound; beyond it the cooldown error surfaces. Absent → no wait.
47
+ const INFERRED_COOLDOWN_MS = 5 * 60_000;
48
+ const quotaStrikes = new Map();
49
+
50
+ function cooldownPolicyOf(instance) {
51
+ const policy = instance?.accountCooldownPolicy;
52
+ return policy && typeof policy === 'object' ? policy : null;
53
+ }
54
+
55
+ function finiteMs(value, fallback) {
56
+ const n = Number(value);
57
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
58
+ }
59
+
60
+ function policyCooldownMs(policy, strikeKey, reason) {
61
+ if (reason === 'rate_limited') return finiteMs(policy.rateLimitedMs, INFERRED_COOLDOWN_MS);
62
+ if (reason === 'quota' && Array.isArray(policy.quotaStepsMs) && policy.quotaStepsMs.length) {
63
+ const strikes = quotaStrikes.get(strikeKey) || 0;
64
+ quotaStrikes.set(strikeKey, strikes + 1);
65
+ return finiteMs(policy.quotaStepsMs[Math.min(strikes, policy.quotaStepsMs.length - 1)], INFERRED_COOLDOWN_MS);
66
+ }
67
+ return finiteMs(policy.unknownMs, INFERRED_COOLDOWN_MS);
23
68
  }
24
69
 
25
70
  // Burst throttles reopen within seconds. A 429 whose Retry-After points
@@ -75,6 +120,23 @@ function accountsExhaustedError(useCredits) {
75
120
  return error;
76
121
  }
77
122
 
123
+ // Every eligible account is only cooling down after a rate limit (no quota
124
+ // evidence): a rate limit, advising the earliest cooldown end as the wait.
125
+ // The pool already spent its cooldown wait, so this is a terminal verdict for
126
+ // the loop, not an unclassified failure.
127
+ function earliestCooldownMs(rows, now) {
128
+ const ends = rows.map((row) => Number(row.blockedUntil)).filter((until) => Number.isFinite(until) && until > now);
129
+ return ends.length ? Math.min(...ends) - now : null;
130
+ }
131
+
132
+ function accountsCoolingDownError(rows, now) {
133
+ const error = new Error('Every connected account is cooling down after a rate limit. Try again shortly.');
134
+ error.code = 'rate_limited';
135
+ const wait = earliestCooldownMs(rows, now);
136
+ if (wait != null) error.retryAfterMs = wait;
137
+ return markProviderRecoveryExhausted(error, { owner: 'account-pool', terminal: true });
138
+ }
139
+
78
140
  // Structured proof that the send ended because no account can serve `model`
79
141
  // (not merely because a generic 429 or a stopped loop surfaced an error).
80
142
  function quotaExhausted(error, providerName, model, rows, options = {}) {
@@ -116,28 +178,135 @@ function boundAccountSend(providerName, row, messages, options) {
116
178
  return { opts, history, emitted: () => emitted };
117
179
  }
118
180
 
119
- // Commit only a successful fallback, without overwriting a newer selection.
120
- function commitFallbackAccount(providerName, pool, row) {
121
- if (row.id === pool.selectedId) return;
122
- const current = readProviderAccountPool(providerName);
123
- if (current.selectedId === pool.selectedId && current.accounts.some((entry) => entry.id === row.id)) {
124
- return changeProviderAccounts(providerName, { selectedId: row.id });
181
+ // Commit only a successful fallback, without overwriting a newer selection:
182
+ // the selection is compared under the write's lock, not before it. A fallback
183
+ // forced by a model-scoped limit ("7D Fable" full, or a block attributed to
184
+ // one family) serves this request only: the selected account still serves
185
+ // every other model, so it stays the provider's account of record.
186
+ async function commitFallbackAccount(providerName, pool, row, model, refusalScoped, policy) {
187
+ if (row.id === pool.selectedId || refusalScoped) return null;
188
+ if (accountExhaustedForModelOnly(providerName, pool.selectedId, model)) return null;
189
+ return selectProviderAccountIf(providerName, pool.selectedId, row.id, policy);
190
+ }
191
+
192
+ // The account each session runs on under the headroom policy. Every answer
193
+ // records its provider and account in the assistant message's
194
+ // `providerMetadata` (and in provider state when the adapter keeps one), so
195
+ // the binding survives a restart and eviction from this map, which only
196
+ // spares the history walk. Bounded, oldest binding first out.
197
+ const SESSION_ACCOUNT_LIMIT = 1000;
198
+ const ROUTING_METADATA_KEY = 'accountRouting';
199
+ const sessionAccounts = new Map();
200
+
201
+ // Forget every in-memory binding, as a restart does (tests).
202
+ export function resetSessionAccountBindings() {
203
+ sessionAccounts.clear();
204
+ }
205
+
206
+ function rememberSessionAccount(key, id) {
207
+ sessionAccounts.delete(key);
208
+ sessionAccounts.set(key, id);
209
+ if (sessionAccounts.size > SESSION_ACCOUNT_LIMIT) sessionAccounts.delete(sessionAccounts.keys().next().value);
210
+ }
211
+
212
+ function accountExhaustedForModelOnly(providerName, id, model) {
213
+ const pool = readProviderAccountPool(providerName);
214
+ const row = pool.accounts.find((entry) => entry.id === id);
215
+ return (
216
+ Boolean(row) &&
217
+ providerAccountExhaustedForModelOnly(row, Date.now(), model, { useCredits: pool.useCredits === true })
218
+ );
219
+ }
220
+
221
+ // Only a binding recorded for THIS provider counts: a session that switched
222
+ // providers carries another roster's account ids (every roster has a `default`).
223
+ // The session's binding is read before the serving account, which differs
224
+ // when a model-scoped limit lent this one answer to another account.
225
+ function sessionAccountId(providerName, key, messages, options) {
226
+ if (key && sessionAccounts.has(key)) return sessionAccounts.get(key);
227
+ const state = options.providerState;
228
+ if (state?.providerAccountId && state.providerAccountProvider === providerName) {
229
+ return state.providerSessionAccountId || state.providerAccountId;
125
230
  }
231
+ for (let index = (messages?.length || 0) - 1; index >= 0; index -= 1) {
232
+ const routing = messages[index]?.providerMetadata?.[ROUTING_METADATA_KEY];
233
+ if (routing?.accountId && routing.provider === providerName) return routing.sessionAccountId || routing.accountId;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ // Stamp the serving account on the answer, independent of transport state,
239
+ // with the account the session stays bound to (`bindingId`).
240
+ function recordServingAccount(providerName, result, id, bindingId) {
241
+ if (!result || typeof result !== 'object') return;
242
+ if (result.providerReplay) result.providerReplay = { ...result.providerReplay, accountId: id };
243
+ if (result.providerState) {
244
+ result.providerState = {
245
+ ...result.providerState,
246
+ providerAccountId: id,
247
+ providerSessionAccountId: bindingId,
248
+ providerAccountProvider: providerName,
249
+ };
250
+ }
251
+ const metadata =
252
+ result.providerMetadata && typeof result.providerMetadata === 'object' ? result.providerMetadata : {};
253
+ result.providerMetadata = {
254
+ ...metadata,
255
+ [ROUTING_METADATA_KEY]: { provider: providerName, accountId: id, sessionAccountId: bindingId },
256
+ };
126
257
  }
127
258
 
128
259
  // Whether the failed account's quota window is exhausted, plus the server's
129
260
  // retry delay. Anthropic and other subscriptions may use the same 429 code
130
261
  // for burst throttling and exhausted subscription windows, so the native
131
262
  // usage endpoint decides rather than a human-readable message.
263
+ // `until` is when the block lifts: the real quota reset when the provider
264
+ // named one, else the advised delay, else an inferred five minutes. A long
265
+ // Retry-After without quota evidence is a rate-limit cooldown: it rotates
266
+ // this send, blocks no longer than advised and is never quota evidence.
132
267
  async function accountQuotaExhausted(providerName, row, model, error, account) {
133
- const status = Number(error?.status || error?.httpStatus || error?.response?.status || 0);
134
- const delay = retryAfterMsFromError(error);
268
+ const { kind, status, retryAfterMs: delay, resetAt } = providerErrorKind(error);
269
+ const now = Date.now();
270
+ const policy = cooldownPolicyOf(account(row.id));
271
+ const floorMs = policy ? finiteMs(policy.floorMs, 0) : 0;
272
+ const blockUntil = (withReset, reason = 'unknown') => {
273
+ if (withReset && resetAt > now) return { until: resetAt, source: 'retry-after' };
274
+ if (delay > 0) return { until: now + Math.max(delay, floorMs), source: 'retry-after' };
275
+ if (policy) {
276
+ const ms = policyCooldownMs(policy, `${providerName}:${row.id}`, reason);
277
+ return { until: now + Math.max(ms, floorMs), source: 'inferred' };
278
+ }
279
+ return { until: now + INFERRED_COOLDOWN_MS, source: 'inferred' };
280
+ };
281
+ if (SERVER_SIDE_KINDS.has(kind) || isExplicitUserAbortError(error)) {
282
+ return { exhausted: false, delay, evidence: false, ...blockUntil(false) };
283
+ }
284
+ // An account-level block (`accountBlocked`, e.g. the account needs
285
+ // verification): with a policy `blockedMs` that account cools for it and the
286
+ // roster rotates; never quota evidence. Without one it surfaces as before.
287
+ const blockedMs = policy ? finiteMs(policy.blockedMs, 0) : 0;
288
+ if (error?.accountBlocked === true && blockedMs > 0) {
289
+ return {
290
+ exhausted: true,
291
+ accountBlocked: true,
292
+ delay,
293
+ evidence: false,
294
+ until: now + Math.max(blockedMs, floorMs),
295
+ source: 'inferred',
296
+ };
297
+ }
135
298
  const typed = isAccountQuotaError(error);
136
- let exhausted = typed || (status === 429 && delay > QUOTA_WINDOW_RETRY_AFTER_MS);
299
+ if (typed) return { exhausted: true, delay, evidence: true, ...blockUntil(true, 'quota') };
300
+ // A policy with rotateOnRateLimit rotates on a plain rate limit too (the
301
+ // advice as its cooldown, else rateLimitedMs); never quota evidence.
302
+ if (policy?.rotateOnRateLimit === true && kind === 'rate_limited' && status === 429) {
303
+ return { exhausted: true, delay, evidence: false, ...blockUntil(false, 'rate_limited') };
304
+ }
305
+ let exhausted = status === 429 && delay > QUOTA_WINDOW_RETRY_AFTER_MS;
137
306
  // Proof of a spent quota: a typed quota refusal or an exhausted usage window
138
307
  // (a long Retry-After alone only rotates the roster, it proves nothing).
139
- let evidence = typed;
140
- if (!typed && status === 429) {
308
+ let evidence = false;
309
+ if (status === 429) {
141
310
  await fetchOAuthUsageSnapshot({ provider: providerName, accountId: row.id }, account(row.id), () => {}, {
142
311
  force: true,
143
312
  });
@@ -147,7 +316,7 @@ async function accountQuotaExhausted(providerName, row, model, error, account) {
147
316
  evidence = providerAccountExhausted({ ...entry, blockedUntil: 0 }, Date.now(), model, options);
148
317
  exhausted = exhausted || evidence || providerAccountExhausted(entry, Date.now(), model, options);
149
318
  }
150
- return { exhausted, delay, evidence };
319
+ return { exhausted, delay, evidence, ...(exhausted ? blockUntil(evidence, evidence ? 'quota' : 'unknown') : {}) };
151
320
  }
152
321
 
153
322
  // The pool's send: walks the eligible accounts until one answers, blocking
@@ -158,12 +327,20 @@ function poolSend(providerName, account) {
158
327
  let lastError;
159
328
  let probed = false;
160
329
  const evidenced = new Set();
330
+ const sessionKey = options.sessionId ? `${providerName}:${options.sessionId}` : null;
331
+ const boundId = sessionAccountId(providerName, sessionKey, messages, options);
332
+ // Whether this send already left an account, and whether the latest quota
333
+ // refusal held only for this model's family.
334
+ let switched = false;
335
+ let refusalScoped = false;
161
336
  // Mark `error` only when every account in `ids` (default: the roster) is
162
337
  // exhausted with quota evidence: observed in this send, or a full window.
163
- const markExhausted = (error, ids) => {
338
+ // Without an error of its own, the send reports quota exhaustion only on
339
+ // that evidence, and a rate-limit cooldown otherwise.
340
+ const exhaustion = (ids) => {
164
341
  const pool = readProviderAccountPool(providerName);
165
- // Auto switching off: the eligible set is the selected account alone.
166
- const scope = ids || (pool.auto === false ? [pool.selectedId] : null);
342
+ // Manual policy: the eligible set is the selected account alone.
343
+ const scope = ids || (providerAccountPolicy(pool) === 'manual' ? [pool.selectedId] : null);
167
344
  const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
168
345
  const opts = { useCredits: pool.useCredits === true };
169
346
  const now = Date.now();
@@ -177,9 +354,47 @@ function poolSend(providerName, account) {
177
354
  opts
178
355
  )
179
356
  );
180
- return all ? quotaExhausted(error, providerName, model, rows, opts) : error;
357
+ return { rows, opts, now, all };
358
+ };
359
+ const markExhausted = (error, ids) => {
360
+ const { rows, opts, now, all } = exhaustion(ids);
361
+ if (all) {
362
+ return quotaExhausted(error || accountsExhaustedError(opts.useCredits), providerName, model, rows, opts);
363
+ }
364
+ return error || accountsCoolingDownError(rows, now);
365
+ };
366
+ // Accounts this send blocked on a rate-limit cooldown, the time it spent
367
+ // waiting for cooldowns, and the provider's wait bound.
368
+ const cooled = new Set();
369
+ let waitedMs = 0;
370
+ let sends = 0;
371
+ const waitBoundMs = (ids) => {
372
+ const id = ids?.[0] || readProviderAccountPool(providerName).selectedId || 'default';
373
+ return finiteMs(cooldownPolicyOf(account(id))?.waitForCooldownMs, 0);
181
374
  };
182
- while (attempted.size < 20) {
375
+ // A cooldown-only eligible set whose earliest cooldown ends inside the
376
+ // remaining wait budget is waited out; otherwise the send fails as before.
377
+ const waitOrThrow = async (error, ids) => {
378
+ const { rows, now, all } = exhaustion(ids);
379
+ const wait = all ? null : earliestCooldownMs(rows, now);
380
+ if (wait == null || waitedMs + wait > waitBoundMs(ids)) throw markExhausted(error, ids);
381
+ options.onStageChange?.('reconnecting', {
382
+ message: `Rate limited — every account is cooling down; retrying in ${Math.ceil(wait / 1000)}s`,
383
+ reason: 'rate_limited',
384
+ waitMs: wait,
385
+ retryAt: now + wait,
386
+ });
387
+ await sleepWithAbort(wait, options.signal);
388
+ waitedMs += wait;
389
+ const lifted = Date.now();
390
+ for (const row of readProviderAccountPool(providerName).accounts) {
391
+ if (cooled.has(row.id) && !(Number(row.blockedUntil) > lifted)) {
392
+ cooled.delete(row.id);
393
+ attempted.delete(row.id);
394
+ }
395
+ }
396
+ };
397
+ while (attempted.size < 20 && sends < 20) {
183
398
  options.signal?.throwIfAborted();
184
399
  const pool = readProviderAccountPool(providerName);
185
400
  if (!pool.accounts.length) {
@@ -194,7 +409,8 @@ function poolSend(providerName, account) {
194
409
  }
195
410
  return account('default').send(messages, model, tools, options);
196
411
  }
197
- const row = chooseProviderAccount(pool, attempted, Date.now(), model);
412
+ const policy = providerAccountPolicy(pool);
413
+ const row = chooseProviderAccount(pool, attempted, Date.now(), model, { sessionAccountId: boundId });
198
414
  if (!row) {
199
415
  // Once per send: a refusal built on a stale meter must not outlive the
200
416
  // meter. Re-measuring can only widen the roster, so it is tried before
@@ -203,15 +419,27 @@ function poolSend(providerName, account) {
203
419
  probed = true;
204
420
  if (await probeRefusedAccounts(providerName, pool, model, account)) continue;
205
421
  }
206
- throw markExhausted(lastError || accountsExhaustedError(pool.useCredits === true));
422
+ await waitOrThrow(lastError);
423
+ continue;
207
424
  }
208
425
  attempted.add(row.id);
426
+ sends += 1;
209
427
  const bound = boundAccountSend(providerName, row, messages, options);
210
428
  try {
211
429
  const result = await account(row.id).send(bound.history, model, tools, bound.opts);
212
- if (result?.providerReplay) result.providerReplay = { ...result.providerReplay, accountId: row.id };
213
- if (result?.providerState) result.providerState = { ...result.providerState, providerAccountId: row.id };
214
- if (commitFallbackAccount(providerName, pool, row)) {
430
+ quotaStrikes.delete(`${providerName}:${row.id}`);
431
+ // Headroom policy: a session's first pick, or its move off its account
432
+ // on a limit, becomes the selection; continuing on its account does
433
+ // not, so concurrent sessions never flip the selection back and forth.
434
+ // A move forced by a model-scoped limit serves this request only.
435
+ const leftBound = Boolean(boundId) && row.id !== boundId;
436
+ const scoped = switched
437
+ ? refusalScoped
438
+ : policy === 'headroom' && leftBound && accountExhaustedForModelOnly(providerName, boundId, model);
439
+ recordServingAccount(providerName, result, row.id, scoped && boundId ? boundId : row.id);
440
+ if (sessionKey && policy === 'headroom' && !scoped) rememberSessionAccount(sessionKey, row.id);
441
+ const commit = policy !== 'headroom' || !boundId || leftBound;
442
+ if (commit && (await commitFallbackAccount(providerName, pool, row, model, scoped, policy))) {
215
443
  // Publish only a committed selection, never a speculative retry or
216
444
  // one superseded by a manual switch. The existing progress channel
217
445
  // carries this across runtime shards to the attached UI.
@@ -230,7 +458,7 @@ function poolSend(providerName, account) {
230
458
  if (options.signal?.aborted) throw error;
231
459
  if (error?.reauthRequired === true) {
232
460
  if (
233
- pool.auto === false ||
461
+ policy === 'manual' ||
234
462
  bound.emitted() ||
235
463
  error.unsafeToRetry ||
236
464
  error.liveTextEmitted ||
@@ -238,25 +466,47 @@ function poolSend(providerName, account) {
238
466
  )
239
467
  throw error;
240
468
  lastError = error;
469
+ switched = true;
470
+ refusalScoped = false;
241
471
  options.onStageChange?.('reconnecting', { message: 'Sign-in expired — switching to the next account' });
242
472
  continue;
243
473
  }
244
- const { exhausted, delay, evidence } = await accountQuotaExhausted(providerName, row, model, error, account);
245
- if (options.signal?.aborted || !exhausted) throw error;
246
- if (evidence) evidenced.add(row.id);
247
- blockProviderAccount(
474
+ const { exhausted, evidence, until, source, accountBlocked } = await accountQuotaExhausted(
248
475
  providerName,
249
- row.id,
250
- Date.now() + (delay > 0 ? delay : 5 * 60_000),
476
+ row,
251
477
  model,
252
- delay > 0 ? 'retry-after' : 'inferred',
253
- { quota: evidence === true }
478
+ error,
479
+ account
254
480
  );
255
- if (bound.emitted() || error.unsafeToRetry || error.liveTextEmitted || error.emittedToolCall) throw error;
256
- // Auto switching off: the eligible set is the selected account alone.
257
- if (pool.auto === false) throw markExhausted(error, [row.id]);
481
+ if (options.signal?.aborted || !exhausted) throw error;
482
+ if (evidence) evidenced.add(row.id);
483
+ else cooled.add(row.id);
484
+ await blockProviderAccount(providerName, row.id, until, model, source, { quota: evidence === true });
485
+ // An account block is unsafe to re-issue on THAT account only; with no
486
+ // output emitted, the next account may serve the same request.
487
+ if (
488
+ bound.emitted() ||
489
+ (error.unsafeToRetry && accountBlocked !== true) ||
490
+ error.liveTextEmitted ||
491
+ error.emittedToolCall
492
+ )
493
+ throw error;
494
+ // Manual policy: the eligible set is the selected account alone.
495
+ if (policy === 'manual') {
496
+ await waitOrThrow(error, [row.id]);
497
+ lastError = error;
498
+ continue;
499
+ }
258
500
  lastError = error;
259
- options.onStageChange?.('reconnecting', { message: 'Quota exhausted — switching to the next account' });
501
+ switched = true;
502
+ refusalScoped = accountExhaustedForModelOnly(providerName, row.id, model);
503
+ options.onStageChange?.('reconnecting', {
504
+ message: evidence
505
+ ? 'Quota exhausted — switching to the next account'
506
+ : accountBlocked
507
+ ? 'Account blocked — switching to the next account'
508
+ : 'Rate limited (cooldown) — switching to the next account',
509
+ });
260
510
  }
261
511
  }
262
512
  throw markExhausted(lastError || new Error('Provider account attempts exhausted.'));
@@ -9,6 +9,7 @@
9
9
  import { PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS, createTimeoutSignal } from '../stall-policy.mjs';
10
10
  import { AnthropicFallbackTriggeredError, markProviderRecoveryExhausted } from './retry-classifier.mjs';
11
11
  import { normalizeAnthropicNonStreamingResponse } from './lib/anthropic-request-utils.mjs';
12
+ import { capForNonStreaming } from './anthropic-max-tokens-recovery.mjs';
12
13
  import { createAnthropicRecoveryGuards } from './anthropic-recovery-guards.mjs';
13
14
 
14
15
  /**
@@ -44,7 +45,7 @@ export function createAnthropicApiRecovery({
44
45
  try {
45
46
  onStageChange?.('requesting', { transport: 'non-streaming-fallback' });
46
47
  } catch {}
47
- const nonStreamingParams = { ...params, stream: false };
48
+ const nonStreamingParams = capForNonStreaming({ ...params, stream: false });
48
49
  const timeoutMs =
49
50
  Number(opts._nonStreamingTimeoutMs) > 0
50
51
  ? Number(opts._nonStreamingTimeoutMs)
@@ -14,13 +14,16 @@ import {
14
14
  ANTHROPIC_RETRY_JITTER_RATIO,
15
15
  anthropicMaxAttempts,
16
16
  anthropicRequestTimeoutMs,
17
+ AnthropicFallbackTriggeredError,
17
18
  emitProviderRetryStage,
18
19
  withRetry,
19
- retryAfterMsFromError,
20
20
  retryDelayLabel,
21
21
  } from './retry-classifier.mjs';
22
+ import { ANTHROPIC_MAX_CONSECUTIVE_529 } from './retry-backoff.mjs';
22
23
  import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, createTimeoutSignal } from '../stall-policy.mjs';
23
- import { noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
24
+ import { fastModeDowngraded, noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
25
+ import { adjustMaxTokensForContextOverflow } from './anthropic-max-tokens-recovery.mjs';
26
+ import { applyAnthropicErrorContract, retryAfterMsFromHeaders } from './anthropic-error-contract.mjs';
24
27
  import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
25
28
  import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
26
29
 
@@ -78,12 +81,61 @@ export function armFirstByteWatchdog(streamController, midState) {
78
81
  * @param {object} deps.params the streaming request body (mutated on fast-pool de-latch)
79
82
  * @param {object|null} deps.requestHeaders per-call beta/extra headers
80
83
  * @param {AbortSignal|null} deps.totalSignal session-lifetime pass-through
84
+ * @param {{ state: { consecutive529Errors: number } }|null} [deps.overload] the send's shared overload counter
81
85
  */
82
- export function createAnthropicApiTransport({ client, label, opts, useModel, params, requestHeaders, totalSignal }) {
86
+ export function createAnthropicApiTransport({
87
+ client,
88
+ label,
89
+ opts,
90
+ useModel,
91
+ params,
92
+ requestHeaders,
93
+ totalSignal,
94
+ overload = null,
95
+ }) {
83
96
  const requestOptions = (attemptSignal) => ({
84
97
  signal: attemptSignal,
85
98
  ...(requestHeaders ? { headers: requestHeaders } : {}),
86
99
  });
100
+ // The SDK throws a non-2xx before `.asResponse()` resolves, so an HTTP
101
+ // failure arrives as an APIError rather than a Response. Stamp the
102
+ // provider-boundary error contract on it (status, lowercased error type as
103
+ // `code`, preserved headers, uncapped retryAfterMs / resetAt).
104
+ const annotateHttpFailure = (err) => {
105
+ if (err && typeof err === 'object' && Number(err.status || err.httpStatus || 0) > 0) {
106
+ applyAnthropicErrorContract(err, { status: err.status || err.httpStatus, headers: err.headers });
107
+ }
108
+ return err;
109
+ };
110
+ // A fast-pool refusal (or a "Fast mode is not enabled" 400) says nothing
111
+ // about standard-speed capacity: drop `speed` and let the caller replay at
112
+ // once instead of waiting out a window that only the fast pool owns.
113
+ const dropFastSpeedAfter = (body, err) => {
114
+ if (body.speed !== 'fast') return false;
115
+ if (!fastModeDowngraded(noteFastModeCapacityError(err, { fast: true }))) return false;
116
+ delete body.speed;
117
+ return true;
118
+ };
119
+ // In-attempt body repairs, each replayed once: drop speed after a fast-pool
120
+ // refusal, and shrink max_tokens after the "input length and max_tokens
121
+ // exceed context limit" 400.
122
+ const withBodyRecovery = (body, attempt) => async (ctx) => {
123
+ let speedDropped = false;
124
+ let tokensAdjusted = false;
125
+ for (;;) {
126
+ try {
127
+ return await attempt(ctx);
128
+ } catch (err) {
129
+ if (!speedDropped && dropFastSpeedAfter(body, err)) {
130
+ speedDropped = true;
131
+ } else if (!tokensAdjusted && adjustMaxTokensForContextOverflow(body, err?.status, err?.message)) {
132
+ tokensAdjusted = true;
133
+ } else {
134
+ throw err;
135
+ }
136
+ }
137
+ }
138
+ };
87
139
  // Initial-response retry policy shared by the streaming request and the
88
140
  // non-streaming fallback.
89
141
  const retryPolicy = (signal, perAttemptLabel, recoveryOwner) => ({
@@ -95,23 +147,27 @@ export function createAnthropicApiTransport({ client, label, opts, useModel, par
95
147
  perAttemptTimeoutMs: anthropicRequestTimeoutMs(),
96
148
  perAttemptLabel,
97
149
  provider: 'anthropic',
150
+ // This transport honors the positive x-should-retry override.
151
+ honorShouldRetryTrue: true,
98
152
  recoveryOwner,
99
153
  retry529: opts.retry529,
100
154
  model: useModel,
101
155
  fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
156
+ // One overload counter for the send: SSE overloads count toward the same
157
+ // limit as request-loop 529s.
158
+ overloadState: overload?.state,
159
+ overloadFallbackAfter: ANTHROPIC_MAX_CONSECUTIVE_529,
160
+ stopAfterRepeated529: overload?.stopAfterRepeated529 === true,
102
161
  });
103
162
 
104
- const requestStreamingResponse = () =>
105
- withRetry(
106
- async ({ signal: attemptSignal }) => {
107
- noteRequestServiceTier(params.speed);
163
+ const requestStreamingResponse = async () => {
164
+ const attempt = async ({ signal: attemptSignal }) => {
165
+ noteRequestServiceTier(params.speed);
166
+ try {
108
167
  const res = await client.messages.create(params, requestOptions(attemptSignal)).asResponse();
109
168
  if (!res.ok) {
110
169
  const text = await res.text().catch(() => '');
111
170
  const err = new Error(`Anthropic API ${res.status}: ${text.slice(0, 200)}`);
112
- err.status = res.status;
113
- err.httpStatus = res.status;
114
- err.initialResponseError = true;
115
171
  // Carry response headers so withRetry can honor a
116
172
  // short Retry-After and upstream can read quota hints.
117
173
  err.headers = res.headers;
@@ -120,48 +176,55 @@ export function createAnthropicApiTransport({ client, label, opts, useModel, par
120
176
  // output/tool exposure, so the request-local
121
177
  // withRetry loop may retry it with jitter.
122
178
  if (res.status === 429) {
123
- const retryAfterMs = retryAfterMsFromError({
124
- headers: res.headers,
125
- response: { headers: res.headers },
126
- });
127
179
  err.name = 'ProviderQuotaError';
128
- err.code = 'PROVIDER_QUOTA';
129
- err.retryAfterMs = retryAfterMs;
180
+ err.retryAfterMs = retryAfterMsFromHeaders(res.headers);
130
181
  err.providerQuota = true;
131
182
  err.quotaExceeded = true;
132
183
  }
133
- throw err;
184
+ throw applyAnthropicErrorContract(err, { status: res.status, headers: res.headers, bodyText: text });
134
185
  }
135
186
  if (!res.body) {
136
187
  throw new Error('Anthropic streaming response has no body');
137
188
  }
138
189
  return res;
139
- },
140
- {
190
+ } catch (err) {
191
+ throw annotateHttpFailure(err);
192
+ }
193
+ };
194
+ try {
195
+ return await withRetry(withBodyRecovery(params, attempt), {
141
196
  ...retryPolicy(totalSignal, `${label} Anthropic streaming response`, `${label}-initial-response`),
142
- onRetry: ({ attempt, maxAttempts, lastErr, delayMs, delayReason }) => {
143
- emitProviderRetryStage(opts.onStageChange, { attempt: attempt + 1, maxAttempts, lastErr, delayMs });
144
- // Long/unknown fast-pool window: replay at
145
- // standard speed rather than waiting it out.
146
- if (params?.speed === 'fast' && noteFastModeCapacityError(lastErr, { fast: true }) !== 'retry-fast') {
147
- delete params.speed;
148
- }
197
+ onRetry: ({ attempt: retryNo, maxAttempts, lastErr, delayMs, delayReason }) => {
198
+ emitProviderRetryStage(opts.onStageChange, { attempt: retryNo + 1, maxAttempts, lastErr, delayMs });
149
199
  const status = Number(lastErr?.httpStatus || lastErr?.status || lastErr?.response?.status || 0);
150
200
  if (status === 429) notifyCurrentAnthropicRateLimit(lastErr);
151
201
  const delayLabel = retryDelayLabel(delayMs, delayReason);
152
202
  process.stderr.write(
153
- `[${label}] retry attempt ${attempt + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`
203
+ `[${label}] retry attempt ${retryNo + 1} after ${lastErr?.message || lastErr?.code || 'transient error'}${delayLabel}\n`
154
204
  );
155
205
  },
206
+ });
207
+ } catch (err) {
208
+ // Whatever escapes withRetry failed before any stream existed and has
209
+ // already spent the request-level budget: mark it so mid-stream
210
+ // recovery never grants it a second, SSE-sized retry budget.
211
+ if (err && typeof err === 'object' && !(err instanceof AnthropicFallbackTriggeredError)) {
212
+ err.initialResponseError = true;
156
213
  }
157
- );
214
+ throw err;
215
+ }
216
+ };
158
217
 
159
218
  const requestNonStreamingMessage = (nonStreamingParams, signal) =>
160
219
  withRetry(
161
- async ({ signal: attemptSignal }) => {
220
+ withBodyRecovery(nonStreamingParams, async ({ signal: attemptSignal }) => {
162
221
  noteRequestServiceTier(nonStreamingParams.speed);
163
- return client.messages.create(nonStreamingParams, requestOptions(attemptSignal));
164
- },
222
+ try {
223
+ return await client.messages.create(nonStreamingParams, requestOptions(attemptSignal));
224
+ } catch (err) {
225
+ throw annotateHttpFailure(err);
226
+ }
227
+ }),
165
228
  {
166
229
  ...retryPolicy(signal, `${label} Anthropic non-streaming fallback`, `${label}-nonstreaming-request`),
167
230
  onRetry: ({ attempt, maxAttempts, lastErr, delayMs }) =>