mixdog 1.3.5 → 1.4.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 (341) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +2 -2
  3. package/package.json +2 -11
  4. package/scripts/daemon-stub.mjs +1 -1
  5. package/scripts/gate-local.mjs +4 -2
  6. package/scripts/native-release-status.mjs +2 -2
  7. package/scripts/recall-history-cases.json +1 -1
  8. package/scripts/suite-health.mjs +10 -1
  9. package/scripts/test.mjs +7 -11
  10. package/src/agents/reviewer/AGENT.md +7 -11
  11. package/src/agents/reviewer/agent.json +1 -1
  12. package/src/defaults/shipped-history.json +17 -1
  13. package/src/defaults/skills/browser-use/SKILL.md +59 -11
  14. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  15. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  16. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  17. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  18. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  19. package/src/defaults/skills/setup/SKILL.md +22 -0
  20. package/src/defaults/skills/setup/references/actions.md +39 -5
  21. package/src/defaults/skills/setup/references/surfaces.md +11 -2
  22. package/src/defaults/skills/visualize/SKILL.md +2 -1
  23. package/src/lib/rules-builder.cjs +10 -8
  24. package/src/rules/agent/AGENT.md +6 -6
  25. package/src/rules/lead/LEAD.md +18 -20
  26. package/src/rules/shared/05-tool-calls.md +5 -6
  27. package/src/rules/shared/10-tool-workflow.md +2 -0
  28. package/src/rules/shared/60-verification.md +7 -5
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/config.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  35. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  36. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
  39. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
  40. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
  41. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  42. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  45. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
  46. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  47. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  48. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  49. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
  50. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  52. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
  53. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  54. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  55. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  56. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  57. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  58. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  64. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  65. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  68. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  69. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  72. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  76. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  77. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
  78. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  79. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
  82. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
  83. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  84. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  86. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
  88. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  90. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  92. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
  93. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  94. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  97. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  99. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  101. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  102. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  103. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  104. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  105. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  106. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  107. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  109. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  110. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  112. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  113. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  114. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  115. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  116. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  117. package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
  118. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  120. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  121. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  122. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  123. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  124. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  125. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  126. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  127. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
  128. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  129. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  130. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  131. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  132. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  133. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  135. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
  136. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  138. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  141. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  143. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  144. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  145. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  146. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  147. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  148. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  149. package/src/runtime/browser-bridge/client.mjs +73 -8
  150. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  151. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  152. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  153. package/src/runtime/channels/lib/config.mjs +6 -61
  154. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  155. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  156. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  157. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  158. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  159. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  160. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  161. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  162. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  163. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  164. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  165. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  166. package/src/runtime/channels/lib/scheduler.mjs +33 -147
  167. package/src/runtime/channels/lib/schedules-db.mjs +34 -11
  168. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  169. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
  170. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  171. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  172. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  173. package/src/runtime/channels/lib/worker-main.mjs +10 -34
  174. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  175. package/src/runtime/computer-bridge/actions.mjs +9 -1
  176. package/src/runtime/computer-bridge/client.mjs +180 -8
  177. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  178. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  179. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  180. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  181. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  182. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  183. package/src/runtime/connectors/google-mcp.mjs +10 -12
  184. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  185. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  186. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  187. package/src/runtime/session-search/session-search-db.mjs +73 -9
  188. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  189. package/src/runtime/shared/abort-race.mjs +89 -0
  190. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  191. package/src/runtime/shared/agent-kind.mjs +4 -3
  192. package/src/runtime/shared/bounded-download.mjs +8 -3
  193. package/src/runtime/shared/config.mjs +2 -13
  194. package/src/runtime/shared/connector-accounts.mjs +20 -10
  195. package/src/runtime/shared/connector-catalog.d.mts +29 -0
  196. package/src/runtime/shared/connector-catalog.mjs +150 -0
  197. package/src/runtime/shared/connector-triggers.mjs +22 -19
  198. package/src/runtime/shared/defaults-separation.mjs +2 -14
  199. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  200. package/src/runtime/shared/err-text.mjs +24 -15
  201. package/src/runtime/shared/file-permissions.d.mts +0 -8
  202. package/src/runtime/shared/file-permissions.mjs +0 -11
  203. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  204. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  205. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  206. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  207. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  208. package/src/runtime/shared/mixy-presence.mjs +22 -0
  209. package/src/runtime/shared/orchestration.d.mts +2 -0
  210. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  211. package/src/runtime/shared/profile-config.cjs +1 -1
  212. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  213. package/src/runtime/shared/prompt-origin.mjs +21 -0
  214. package/src/runtime/shared/provider-accounts.mjs +7 -1
  215. package/src/runtime/shared/provider-display-name.mjs +29 -0
  216. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  217. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  218. package/src/runtime/shared/routine-trigger.mjs +10 -0
  219. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  220. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  221. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  222. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  223. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  224. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  225. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  226. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  227. package/src/runtime/terminal-bridge/client.mjs +10 -1
  228. package/src/runtime/visualize-bridge/client.mjs +27 -8
  229. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  230. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  231. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  232. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  233. package/src/session-runtime/agent-home-delivery.mjs +448 -185
  234. package/src/session-runtime/agent-task-sessions.mjs +329 -180
  235. package/src/session-runtime/boot/apis.mjs +9 -0
  236. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  237. package/src/session-runtime/boot/providers.mjs +3 -0
  238. package/src/session-runtime/boot/tools.mjs +6 -1
  239. package/src/session-runtime/config-lifecycle.mjs +6 -0
  240. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  241. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  242. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  243. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  244. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  245. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  246. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  247. package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
  248. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  249. package/src/session-runtime/mixy-heartbeat.mjs +153 -33
  250. package/src/session-runtime/notification-bus.mjs +31 -117
  251. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  252. package/src/session-runtime/notification-listeners.mjs +76 -0
  253. package/src/session-runtime/owned-automation-run.mjs +2 -1
  254. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  255. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  256. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  257. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  258. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  259. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  260. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  261. package/src/session-runtime/routine-owner.mjs +40 -9
  262. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  263. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  264. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  265. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  266. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  267. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  268. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  269. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  270. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  271. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  272. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  273. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  274. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  275. package/src/session-runtime/services/channel-admin.mjs +7 -13
  276. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  277. package/src/session-runtime/services/seeds.mjs +0 -28
  278. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  279. package/src/session-runtime/session-create/session-options.mjs +1 -0
  280. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  281. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  282. package/src/session-runtime/settings-system-api.mjs +13 -1
  283. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  284. package/src/session-runtime/setup-tool/executor.mjs +145 -22
  285. package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
  286. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  287. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
  288. package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
  289. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  290. package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
  291. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  292. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  293. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  294. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  295. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  296. package/src/standalone/channel-transport.mjs +1 -1
  297. package/src/standalone/daemon-channels-loader.mjs +10 -8
  298. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  299. package/src/standalone/daemon-session-control/request.mjs +51 -0
  300. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  301. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  302. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  303. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  304. package/src/standalone/daemon-session-control.mjs +124 -188
  305. package/src/standalone/daemon.mjs +26 -3
  306. package/src/standalone/session-client.mjs +7 -3
  307. package/src/standalone/session-protocol.mjs +5 -0
  308. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  309. package/src/standalone/session-service/quota-resume.mjs +3 -0
  310. package/src/standalone/session-service/retention/busy.mjs +65 -1
  311. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  312. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  313. package/src/standalone/session-tool-output.mjs +15 -7
  314. package/src/tui/app/model-options.mjs +3 -28
  315. package/src/tui/dist/index.mjs +58 -64
  316. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  317. package/src/tui/session/initial-state.mjs +3 -0
  318. package/src/tui/session/notice-surface.mjs +7 -1
  319. package/src/tui/session/queue-helpers.mjs +3 -42
  320. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  321. package/src/tui/session/session-api/routes-write.mjs +5 -1
  322. package/src/tui/session/session-api/routes.mjs +1 -0
  323. package/src/tui/session/session-api/settings.mjs +17 -1
  324. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  325. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  326. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  327. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  328. package/src/tui/session/session-flow/drain.mjs +118 -149
  329. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  330. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  331. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  332. package/src/tui/session/tool-approval-request.mjs +39 -0
  333. package/src/tui/session/tool-approval-settle.mjs +62 -0
  334. package/src/tui/session/tool-approval.mjs +69 -115
  335. package/src/tui/session/tui-steering-persist.mjs +8 -1
  336. package/src/tui/session/turn-stream.mjs +19 -2
  337. package/src/tui/session/turn.mjs +1 -0
  338. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  339. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  340. package/scripts/code-graph-description-contract.mjs +0 -188
  341. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ACCOUNT_PROVIDERS,
3
+ MAX_ACCOUNTS,
3
4
  accountScopedSessionId,
4
5
  readProviderAccountPool,
5
6
  chooseProviderAccount,
@@ -358,110 +359,182 @@ async function accountQuotaExhausted(providerName, row, model, error, account) {
358
359
  evidence = providerAccountExhausted(providerAccountWithBlocks(entry), Date.now(), model, options);
359
360
  exhausted = exhausted || evidence || providerAccountExhausted(entry, Date.now(), model, options);
360
361
  }
361
- return { exhausted, delay, evidence, ...(exhausted ? blockUntil(evidence, evidence ? 'quota' : 'unknown') : {}) };
362
+ const reason = evidence ? 'quota' : 'unknown';
363
+ return { exhausted, delay, evidence, ...(exhausted ? blockUntil(evidence, reason) : {}) };
364
+ }
365
+
366
+ // Record an exhausted verdict on the roster. An account-level block holds
367
+ // every model, so it carries no model or limit scope.
368
+ function blockRefusedAccount(providerName, accountId, model, error, verdict) {
369
+ const { evidence, until, source, reset, accountBlocked } = verdict;
370
+ return blockProviderAccount(providerName, accountId, until, accountBlocked ? '' : model, source, {
371
+ quota: evidence === true,
372
+ reset: reset === true,
373
+ limit: accountBlocked ? '' : quotaLimitOf(error),
374
+ });
375
+ }
376
+
377
+ /**
378
+ * A refusal `error` that account `accountId` gave outside the pool's send (a
379
+ * cache warm, which never rotates): judged and recorded on the roster exactly
380
+ * as the send would, with `instance` as that account's provider. Resolves
381
+ * whether the account was blocked.
382
+ */
383
+ export async function recordAccountRefusal(providerName, accountId, model, error, instance) {
384
+ const row = readProviderAccountPool(providerName).accounts.find((entry) => entry.id === accountId);
385
+ if (!row) return false;
386
+ const verdict = await accountQuotaExhausted(providerName, row, model, error, () => instance);
387
+ if (!verdict.exhausted) return false;
388
+ await blockRefusedAccount(providerName, row.id, model, error, verdict);
389
+ return true;
390
+ }
391
+
392
+ // Whether every account in `ids` (default: the roster) is exhausted with quota
393
+ // evidence: observed in this send (`evidenced`), or a full window.
394
+ function rosterExhaustion(providerName, model, evidenced, ids) {
395
+ const pool = readProviderAccountPool(providerName);
396
+ // Manual policy: the eligible set is the selected account alone.
397
+ const scope = ids || (providerAccountPolicy(pool) === 'manual' ? [pool.selectedId] : null);
398
+ const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
399
+ const opts = { useCredits: pool.useCredits === true };
400
+ const now = Date.now();
401
+ const all =
402
+ rows.length > 0 &&
403
+ rows.every((entry) =>
404
+ providerAccountExhausted(
405
+ evidenced.has(entry.id) ? entry : providerAccountWithBlocks(entry, (block) => block.blockedQuota === true),
406
+ now,
407
+ model,
408
+ opts
409
+ )
410
+ );
411
+ return { rows, opts, now, all };
412
+ }
413
+
414
+ // Mark `error` only when every account in `ids` is exhausted with quota
415
+ // evidence (`rosterExhaustion`). Without an error of its own, the send reports
416
+ // quota exhaustion only on that evidence, and a rate-limit cooldown otherwise.
417
+ function exhaustedSendError(providerName, model, evidenced, error, ids) {
418
+ const { rows, opts, now, all } = rosterExhaustion(providerName, model, evidenced, ids);
419
+ if (all) {
420
+ return quotaExhausted(error || accountsExhaustedError(opts.useCredits), providerName, model, rows, opts);
421
+ }
422
+ return error || accountsCoolingDownError(rows, now, model);
423
+ }
424
+
425
+ // The provider's bound on the time one send spends waiting for cooldowns.
426
+ function cooldownWaitBoundMs(providerName, account, ids) {
427
+ const id = ids?.[0] || readProviderAccountPool(providerName).selectedId || 'default';
428
+ return finiteMs(cooldownPolicyOf(account(id))?.waitForCooldownMs, 0);
429
+ }
430
+
431
+ // A cooldown-only eligible set whose earliest cooldown ends inside the
432
+ // remaining wait budget is waited out; otherwise the send fails as before.
433
+ // `ledger` is the send's own record of the roster (see `poolSend`).
434
+ async function waitOutCooldownOrThrow(request, ledger, error, ids) {
435
+ const { providerName, model, account, options } = request;
436
+ const { rows, now, all } = rosterExhaustion(providerName, model, ledger.evidenced, ids);
437
+ const bound = cooldownWaitBoundMs(providerName, account, ids);
438
+ // Persisting a short cooldown can outlast its deadline. Release only
439
+ // attempts cooled by this send, and only under an enabled wait policy.
440
+ const expired = bound > 0 && rows.some((row) => ledger.cooled.has(row.id) && !(coolingUntil(row, model) > now));
441
+ let wait = null;
442
+ if (!all) wait = expired ? 0 : earliestCooldownMs(rows, now, model);
443
+ if (wait == null || ledger.waitedMs + wait > bound) {
444
+ throw exhaustedSendError(providerName, model, ledger.evidenced, error, ids);
445
+ }
446
+ options.onStageChange?.('reconnecting', {
447
+ message: `Rate limited — every account is cooling down; retrying in ${Math.ceil(wait / 1000)}s`,
448
+ reason: 'rate_limited',
449
+ waitMs: wait,
450
+ retryAt: now + wait,
451
+ });
452
+ // A zero-duration sleep does not check cancellation, including on the
453
+ // final allowed send when no next loop iteration will check it either.
454
+ options.signal?.throwIfAborted();
455
+ await sleepWithAbort(wait, options.signal);
456
+ ledger.waitedMs += wait;
457
+ const lifted = Date.now();
458
+ for (const row of readProviderAccountPool(providerName).accounts) {
459
+ if (ledger.cooled.has(row.id) && !(coolingUntil(row, model) > lifted)) {
460
+ ledger.cooled.delete(row.id);
461
+ ledger.attempted.delete(row.id);
462
+ }
463
+ }
464
+ }
465
+
466
+ // No roster: with credit use off, a stored reading showing the plan's window
467
+ // spent refuses before the implicit account sends.
468
+ function throwIfPlanQuotaSpent(providerName, pool, model) {
469
+ if (pool.useCredits !== true) {
470
+ const stored = readCachedOAuthUsageSnapshot({ provider: providerName, model: '' });
471
+ // A snapshot cached before per-model gating is stale evidence.
472
+ if (stored?.quotaWindows?.length && !staleQuotaWindows(providerName, stored.quotaWindows)) {
473
+ const row = { usage: { windows: stored.quotaWindows, checkedAt: stored.cachedAt } };
474
+ if (providerAccountExhausted(row, Date.now(), model)) {
475
+ throw quotaExhausted(accountsExhaustedError(false), providerName, model, [row]);
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ // Headroom policy: a session's first pick, or its move off its account on a
482
+ // limit, becomes the selection; continuing on its account does not, so
483
+ // concurrent sessions never flip the selection back and forth. A move forced
484
+ // by a model-scoped limit serves this request only (`scoped`). Stamps the
485
+ // serving account on `result` and resolves whether the selection may commit.
486
+ function bindServedAccount(providerName, result, rowId, model, binding) {
487
+ const { policy, boundId, sessionKey, switched, refusalScoped } = binding;
488
+ const leftBound = Boolean(boundId) && rowId !== boundId;
489
+ const scoped = switched
490
+ ? refusalScoped
491
+ : policy === 'headroom' && leftBound && accountExhaustedForModelOnly(providerName, boundId, model);
492
+ recordServingAccount(providerName, result, rowId, scoped && boundId ? boundId : rowId);
493
+ if (sessionKey && policy === 'headroom' && !scoped) rememberSessionAccount(sessionKey, rowId);
494
+ return { scoped, commit: policy !== 'headroom' || !boundId || leftBound };
495
+ }
496
+
497
+ // A failed attempt is never re-issued once output reached the caller or the
498
+ // attempt was unsafe to retry (`unsafe`, judged by the caller and read only
499
+ // when nothing was emitted).
500
+ function attemptUnretryable(bound, error, unsafe) {
501
+ return Boolean(bound.emitted() || unsafe() || error.liveTextEmitted || error.emittedToolCall);
502
+ }
503
+
504
+ function rotationReason(evidence, accountBlocked) {
505
+ if (evidence) return 'Quota exhausted';
506
+ if (accountBlocked) return 'Account blocked';
507
+ return 'Rate limited (cooldown)';
362
508
  }
363
509
 
364
510
  // The pool's send: walks the eligible accounts until one answers, blocking
365
511
  // the ones whose quota window is exhausted along the way.
366
512
  function poolSend(providerName, account) {
367
513
  return async function send(messages, model, tools, options = {}) {
368
- const attempted = new Set();
514
+ const request = { providerName, model, account, options };
515
+ // This send's record of the roster: the accounts it tried, those it found
516
+ // exhausted with quota evidence, those it blocked on a rate-limit
517
+ // cooldown, and the time it spent waiting for cooldowns.
518
+ const ledger = { attempted: new Set(), evidenced: new Set(), cooled: new Set(), waitedMs: 0 };
369
519
  let lastError;
370
520
  let probed = false;
371
- const evidenced = new Set();
521
+ let sends = 0;
372
522
  const sessionKey = options.sessionId ? `${providerName}:${options.sessionId}` : null;
373
523
  const boundId = sessionAccountId(providerName, sessionKey, messages, options);
374
524
  // Whether this send already left an account, and whether the latest quota
375
525
  // refusal held only for this model's family.
376
526
  let switched = false;
377
527
  let refusalScoped = false;
378
- // Mark `error` only when every account in `ids` (default: the roster) is
379
- // exhausted with quota evidence: observed in this send, or a full window.
380
- // Without an error of its own, the send reports quota exhaustion only on
381
- // that evidence, and a rate-limit cooldown otherwise.
382
- const exhaustion = (ids) => {
383
- const pool = readProviderAccountPool(providerName);
384
- // Manual policy: the eligible set is the selected account alone.
385
- const scope = ids || (providerAccountPolicy(pool) === 'manual' ? [pool.selectedId] : null);
386
- const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
387
- const opts = { useCredits: pool.useCredits === true };
388
- const now = Date.now();
389
- const all =
390
- rows.length > 0 &&
391
- rows.every((entry) =>
392
- providerAccountExhausted(
393
- evidenced.has(entry.id) ? entry : providerAccountWithBlocks(entry, (block) => block.blockedQuota === true),
394
- now,
395
- model,
396
- opts
397
- )
398
- );
399
- return { rows, opts, now, all };
400
- };
401
- const markExhausted = (error, ids) => {
402
- const { rows, opts, now, all } = exhaustion(ids);
403
- if (all) {
404
- return quotaExhausted(error || accountsExhaustedError(opts.useCredits), providerName, model, rows, opts);
405
- }
406
- return error || accountsCoolingDownError(rows, now, model);
407
- };
408
- // Accounts this send blocked on a rate-limit cooldown, the time it spent
409
- // waiting for cooldowns, and the provider's wait bound.
410
- const cooled = new Set();
411
- let waitedMs = 0;
412
- let sends = 0;
413
- const waitBoundMs = (ids) => {
414
- const id = ids?.[0] || readProviderAccountPool(providerName).selectedId || 'default';
415
- return finiteMs(cooldownPolicyOf(account(id))?.waitForCooldownMs, 0);
416
- };
417
- // A cooldown-only eligible set whose earliest cooldown ends inside the
418
- // remaining wait budget is waited out; otherwise the send fails as before.
419
- const waitOrThrow = async (error, ids) => {
420
- const { rows, now, all } = exhaustion(ids);
421
- const bound = waitBoundMs(ids);
422
- // Persisting a short cooldown can outlast its deadline. Release only
423
- // attempts cooled by this send, and only under an enabled wait policy.
424
- const expired = bound > 0 && rows.some((row) => cooled.has(row.id) && !(coolingUntil(row, model) > now));
425
- let wait = null;
426
- if (!all) wait = expired ? 0 : earliestCooldownMs(rows, now, model);
427
- if (wait == null || waitedMs + wait > bound) throw markExhausted(error, ids);
428
- options.onStageChange?.('reconnecting', {
429
- message: `Rate limited — every account is cooling down; retrying in ${Math.ceil(wait / 1000)}s`,
430
- reason: 'rate_limited',
431
- waitMs: wait,
432
- retryAt: now + wait,
433
- });
434
- // A zero-duration sleep does not check cancellation, including on the
435
- // final allowed send when no next loop iteration will check it either.
436
- options.signal?.throwIfAborted();
437
- await sleepWithAbort(wait, options.signal);
438
- waitedMs += wait;
439
- const lifted = Date.now();
440
- for (const row of readProviderAccountPool(providerName).accounts) {
441
- if (cooled.has(row.id) && !(coolingUntil(row, model) > lifted)) {
442
- cooled.delete(row.id);
443
- attempted.delete(row.id);
444
- }
445
- }
446
- };
447
- while (attempted.size < 20 && sends < 20) {
528
+ // One try per connectable account at most (provider-accounts.mjs MAX_ACCOUNTS).
529
+ while (ledger.attempted.size < MAX_ACCOUNTS && sends < MAX_ACCOUNTS) {
448
530
  options.signal?.throwIfAborted();
449
531
  const pool = readProviderAccountPool(providerName);
450
532
  if (!pool.accounts.length) {
451
- if (pool.useCredits !== true) {
452
- const stored = readCachedOAuthUsageSnapshot({ provider: providerName, model: '' });
453
- // A snapshot cached before per-model gating is stale evidence.
454
- if (stored?.quotaWindows?.length && !staleQuotaWindows(providerName, stored.quotaWindows)) {
455
- const row = { usage: { windows: stored.quotaWindows, checkedAt: stored.cachedAt } };
456
- if (providerAccountExhausted(row, Date.now(), model)) {
457
- throw quotaExhausted(accountsExhaustedError(false), providerName, model, [row]);
458
- }
459
- }
460
- }
533
+ throwIfPlanQuotaSpent(providerName, pool, model);
461
534
  return account('default').send(messages, model, tools, options);
462
535
  }
463
536
  const policy = providerAccountPolicy(pool);
464
- const row = chooseProviderAccount(pool, attempted, Date.now(), model, { sessionAccountId: boundId });
537
+ const row = chooseProviderAccount(pool, ledger.attempted, Date.now(), model, { sessionAccountId: boundId });
465
538
  if (!row) {
466
539
  // Once per send: a refusal built on a stale meter must not outlive the
467
540
  // meter. Re-measuring can only widen the roster, so it is tried before
@@ -470,26 +543,22 @@ function poolSend(providerName, account) {
470
543
  probed = true;
471
544
  if (await probeRefusedAccounts(providerName, pool, model, account)) continue;
472
545
  }
473
- await waitOrThrow(lastError);
546
+ await waitOutCooldownOrThrow(request, ledger, lastError);
474
547
  continue;
475
548
  }
476
- attempted.add(row.id);
549
+ ledger.attempted.add(row.id);
477
550
  sends += 1;
478
551
  const bound = boundAccountSend(providerName, row, messages, options);
479
552
  try {
480
553
  const result = await account(row.id).send(bound.history, model, tools, bound.opts);
481
554
  quotaStrikes.delete(`${providerName}:${row.id}`);
482
- // Headroom policy: a session's first pick, or its move off its account
483
- // on a limit, becomes the selection; continuing on its account does
484
- // not, so concurrent sessions never flip the selection back and forth.
485
- // A move forced by a model-scoped limit serves this request only.
486
- const leftBound = Boolean(boundId) && row.id !== boundId;
487
- const scoped = switched
488
- ? refusalScoped
489
- : policy === 'headroom' && leftBound && accountExhaustedForModelOnly(providerName, boundId, model);
490
- recordServingAccount(providerName, result, row.id, scoped && boundId ? boundId : row.id);
491
- if (sessionKey && policy === 'headroom' && !scoped) rememberSessionAccount(sessionKey, row.id);
492
- const commit = policy !== 'headroom' || !boundId || leftBound;
555
+ const { scoped, commit } = bindServedAccount(providerName, result, row.id, model, {
556
+ policy,
557
+ boundId,
558
+ sessionKey,
559
+ switched,
560
+ refusalScoped,
561
+ });
493
562
  if (commit && (await commitFallbackAccount(providerName, pool, row, model, scoped, policy))) {
494
563
  // Publish only a committed selection, never a speculative retry or
495
564
  // one superseded by a manual switch. The existing progress channel
@@ -508,62 +577,41 @@ function poolSend(providerName, account) {
508
577
  } catch (error) {
509
578
  if (options.signal?.aborted) throw error;
510
579
  if (error?.reauthRequired === true) {
511
- if (
512
- policy === 'manual' ||
513
- bound.emitted() ||
514
- error.unsafeToRetry ||
515
- error.liveTextEmitted ||
516
- error.emittedToolCall
517
- )
518
- throw error;
580
+ if (policy === 'manual' || attemptUnretryable(bound, error, () => error.unsafeToRetry)) throw error;
519
581
  lastError = error;
520
582
  switched = true;
521
583
  refusalScoped = false;
522
584
  options.onStageChange?.('reconnecting', { message: 'Sign-in expired — switching to the next account' });
523
585
  continue;
524
586
  }
525
- const { exhausted, evidence, until, source, reset, accountBlocked } = await accountQuotaExhausted(
526
- providerName,
527
- row,
528
- model,
529
- error,
530
- account
531
- );
587
+ const verdict = await accountQuotaExhausted(providerName, row, model, error, account);
588
+ const { exhausted, evidence, accountBlocked } = verdict;
532
589
  if (options.signal?.aborted || !exhausted) throw error;
533
- if (evidence) evidenced.add(row.id);
534
- else cooled.add(row.id);
535
- // An account-level block holds every model, so it carries no model or
536
- // limit scope.
537
- await blockProviderAccount(providerName, row.id, until, accountBlocked ? '' : model, source, {
538
- quota: evidence === true,
539
- reset: reset === true,
540
- limit: accountBlocked ? '' : quotaLimitOf(error),
541
- });
590
+ if (evidence) ledger.evidenced.add(row.id);
591
+ else ledger.cooled.add(row.id);
592
+ await blockRefusedAccount(providerName, row.id, model, error, verdict);
542
593
  // An account block is unsafe to re-issue on THAT account only; with no
543
594
  // output emitted, the next account may serve the same request.
544
- if (
545
- bound.emitted() ||
546
- (error.unsafeToRetry && accountBlocked !== true) ||
547
- error.liveTextEmitted ||
548
- error.emittedToolCall
549
- )
550
- throw error;
595
+ if (attemptUnretryable(bound, error, () => error.unsafeToRetry && accountBlocked !== true)) throw error;
551
596
  // Manual policy: the eligible set is the selected account alone.
552
597
  if (policy === 'manual') {
553
- await waitOrThrow(error, [row.id]);
598
+ await waitOutCooldownOrThrow(request, ledger, error, [row.id]);
554
599
  lastError = error;
555
600
  continue;
556
601
  }
557
602
  lastError = error;
558
603
  switched = true;
559
604
  refusalScoped = accountExhaustedForModelOnly(providerName, row.id, model);
560
- let reason = 'Rate limited (cooldown)';
561
- if (evidence) reason = 'Quota exhausted';
562
- else if (accountBlocked) reason = 'Account blocked';
605
+ const reason = rotationReason(evidence, accountBlocked);
563
606
  options.onStageChange?.('reconnecting', { message: `${reason} — switching to the next account` });
564
607
  }
565
608
  }
566
- throw markExhausted(lastError || new Error('Provider account attempts exhausted.'));
609
+ throw exhaustedSendError(
610
+ providerName,
611
+ model,
612
+ ledger.evidenced,
613
+ lastError || new Error('Provider account attempts exhausted.')
614
+ );
567
615
  };
568
616
  }
569
617
 
@@ -264,7 +264,9 @@ export class ProviderAdmissionScheduler {
264
264
  running.parentAbort = () => {
265
265
  try {
266
266
  controller.abort(item.signal.reason);
267
- } catch {}
267
+ } catch {
268
+ // the controller may already be aborted
269
+ }
268
270
  };
269
271
  item.signal.addEventListener('abort', running.parentAbort, { once: true });
270
272
  }
@@ -301,7 +303,9 @@ export class ProviderAdmissionScheduler {
301
303
  if (running.parentAbort && running.parentSignal) {
302
304
  try {
303
305
  running.parentSignal.removeEventListener('abort', running.parentAbort);
304
- } catch {}
306
+ } catch {
307
+ // the abort listener may already be gone
308
+ }
305
309
  }
306
310
  this.running.delete(running);
307
311
  lane.active -= 1;
@@ -312,6 +316,32 @@ export class ProviderAdmissionScheduler {
312
316
  if (this._canDeleteLane(lane)) this._deleteLane(key, lane);
313
317
  }
314
318
 
319
+ /**
320
+ * Why a request that must not wait (a cache warm) cannot start on lane
321
+ * `key` now, or null: the lane's rate-limit cooldown, or a lane throttled
322
+ * after a 429 that would queue it.
323
+ */
324
+ immediateBlock(key) {
325
+ if (this.closedReason) return 'provider scheduler closed';
326
+ const lane = this.lanes.get(String(key || ''));
327
+ if (!lane) return null;
328
+ if (lane.adaptive && lane.cooldownUntil > this.now()) return 'rate-limit cooldown';
329
+ if (lane.queue.length > 0 || lane.active >= lane.limit) return 'account throttled';
330
+ return null;
331
+ }
332
+
333
+ /** Record a rate limit a request outside `run` (a cache warm) received on
334
+ * lane `key`, into the same cooldown state (and broadcast) as a send's. */
335
+ recordRateLimit(key, error) {
336
+ const laneKey = String(key || '');
337
+ if (!laneKey || this.closedReason) return false;
338
+ const lane = this.lanes.get(laneKey) || createAdmissionLane(laneKey, this.concurrency);
339
+ this.lanes.set(laneKey, lane);
340
+ const recorded = this._recordFailure(laneKey, lane, error);
341
+ if (this._canDeleteLane(lane)) this._deleteLane(laneKey, lane);
342
+ return recorded;
343
+ }
344
+
315
345
  snapshot() {
316
346
  const now = this.now();
317
347
  return {
@@ -428,7 +458,9 @@ export class ProviderAdmissionScheduler {
428
458
  if (item.onAbort && item.signal) {
429
459
  try {
430
460
  item.signal.removeEventListener('abort', item.onAbort);
431
- } catch {}
461
+ } catch {
462
+ // the abort listener may already be gone
463
+ }
432
464
  item.onAbort = null;
433
465
  }
434
466
  }
@@ -453,7 +485,9 @@ export class ProviderAdmissionScheduler {
453
485
  for (const running of this.running) {
454
486
  try {
455
487
  running.controller.abort(this.closedReason);
456
- } catch {}
488
+ } catch {
489
+ // the controller may already be aborted
490
+ }
457
491
  }
458
492
  }
459
493
  }
@@ -476,7 +510,7 @@ function digest(value) {
476
510
  .digest('hex');
477
511
  }
478
512
 
479
- function providerAdmissionKey(providerName, provider) {
513
+ export function providerAdmissionKey(providerName, provider) {
480
514
  const name = String(providerName || provider?.name || 'provider');
481
515
  const stableAnthropicOAuthPath =
482
516
  name.toLowerCase() === 'anthropic-oauth'
@@ -556,7 +590,9 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
556
590
  // time. Provider-local retry remains the sole retry owner.
557
591
  try {
558
592
  opts.onStageChange?.('requesting');
559
- } catch {}
593
+ } catch {
594
+ // the stage observer is advisory
595
+ }
560
596
  const admittedOpts = {
561
597
  ...opts,
562
598
  signal: admissionSignal,
@@ -0,0 +1,165 @@
1
+ // anthropic-cache-warm.mjs — the cache-warm mode both Anthropic providers
2
+ // expose as `sendCacheWarm` (session/cache-warm.mjs): one POST of the
3
+ // request the provider's own builder produces, no retry, fallback, re-send,
4
+ // wait or account rotation, and the stream cut at the first content block
5
+ // once the usage (cache read / write) has been reported. Right before the
6
+ // POST the account gate (cache-warm-admission.mjs) may skip it; a refusal is
7
+ // fed into the shared rate-limit and roster state. The request is bounded by
8
+ // the providers' own watchdogs: the first-byte timeout until the usage
9
+ // arrives, then the SSE idle timeout between events. Usage is handed to
10
+ // `onUsage` as it arrives, so a warm that fails or is aborted after the
11
+ // provider accepted it is still accounted. Every exit cancels the stream and
12
+ // aborts the transport.
13
+ import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, PROVIDER_SSE_IDLE_TIMEOUT_MS } from '../stall-policy.mjs';
14
+ import { updateTurnUsage } from './anthropic-sse-turn/turn-state.mjs';
15
+ import { _anthropicSseError } from './anthropic-sse.mjs';
16
+ import { anthropicQuotaError } from './anthropic-oauth-request/initial-status.mjs';
17
+ import { cacheWarmAccountBlock, cacheWarmBlockedError, recordCacheWarmRefusal } from './cache-warm-admission.mjs';
18
+ import { scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
19
+
20
+ const SSE_LABELS = Object.freeze({ display: 'Anthropic cache warm' });
21
+
22
+ function usageSnapshot(usage) {
23
+ const { raw: _raw, ...rest } = usage;
24
+ return { ...rest };
25
+ }
26
+
27
+ // An abort deadline: `arm(ms)` (re)starts it; it fires `controller.abort`.
28
+ function watchdog(controller) {
29
+ let timer = null;
30
+ return {
31
+ arm(ms, what) {
32
+ if (timer) clearTimeout(timer);
33
+ timer = setTimeout(() => controller.abort(new Error(`cache warm ${what} timed out after ${ms}ms`)), ms);
34
+ timer.unref?.();
35
+ },
36
+ clear() {
37
+ if (timer) clearTimeout(timer);
38
+ timer = null;
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Read a warm's SSE stream until usage is known and the first content block
45
+ * starts, or the message ends. Resolves the usage; a stream error rejects
46
+ * with `error.usage` set to what was reported before it. `onChunk` hears
47
+ * every chunk (the idle watchdog). The stream is cancelled on every exit.
48
+ */
49
+ export async function readCacheWarmStream(response, { onUsage = () => {}, onChunk = () => {} } = {}) {
50
+ const usage = { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, raw: null };
51
+ let reported = false;
52
+ const report = (raw) => {
53
+ if (!raw || typeof raw !== 'object') return;
54
+ updateTurnUsage(usage, raw);
55
+ reported = true;
56
+ onUsage(usageSnapshot(usage));
57
+ };
58
+ const reader = response.body.getReader();
59
+ const decoder = new TextDecoder();
60
+ let buffer = '';
61
+ try {
62
+ for (;;) {
63
+ const { value, done } = await reader.read();
64
+ if (done) break;
65
+ buffer += decoder.decode(value, { stream: true });
66
+ let cut = buffer.indexOf('\n\n');
67
+ while (cut >= 0) {
68
+ const frame = buffer.slice(0, cut);
69
+ buffer = buffer.slice(cut + 2);
70
+ cut = buffer.indexOf('\n\n');
71
+ const data = frame
72
+ .split('\n')
73
+ .filter((line) => line.startsWith('data:'))
74
+ .map((line) => line.slice(5).trimStart())
75
+ .join('\n');
76
+ if (!data) continue;
77
+ let event;
78
+ try {
79
+ event = JSON.parse(data);
80
+ } catch {
81
+ continue;
82
+ }
83
+ if (event?.type === 'error' || event?.error) throw _anthropicSseError(event, SSE_LABELS);
84
+ if (event.type === 'message_start') report(event.message?.usage);
85
+ else if (event.type === 'message_delta') report(event.usage);
86
+ else if (event.type === 'content_block_start' && reported) {
87
+ // The prefix was read (and the cache refreshed) before any content:
88
+ // nothing generated after this point is needed.
89
+ return usageSnapshot(usage);
90
+ } else if (event.type === 'message_stop') return usageSnapshot(usage);
91
+ }
92
+ onChunk(reported);
93
+ }
94
+ } catch (error) {
95
+ if (error && typeof error === 'object' && error.usage === undefined) {
96
+ error.usage = reported ? usageSnapshot(usage) : null;
97
+ }
98
+ throw error;
99
+ } finally {
100
+ await reader.cancel().catch(() => {});
101
+ }
102
+ return usageSnapshot(usage);
103
+ }
104
+
105
+ /**
106
+ * One warm on account instance `instance` (roster account `accountId`, empty
107
+ * for a provider without a roster) of `providerName`: the account gate, then
108
+ * `post(signal)` (the provider's single POST of its built body), then the
109
+ * stream. A refusal reaches the shared rate-limit and roster state; the
110
+ * transport is aborted on every exit.
111
+ */
112
+ export async function sendAnthropicCacheWarm({
113
+ providerName,
114
+ instance,
115
+ accountId = '',
116
+ model,
117
+ opts = {},
118
+ post,
119
+ onUsage,
120
+ }) {
121
+ const blocked = cacheWarmAccountBlock(providerName, instance, {
122
+ accountId,
123
+ model,
124
+ sessionId: opts.session?.id || opts.sessionId || '',
125
+ });
126
+ if (blocked) throw cacheWarmBlockedError(blocked);
127
+ const controller = new AbortController();
128
+ const callerSignal = opts.signal;
129
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
130
+ if (callerSignal?.aborted) controller.abort(callerSignal.reason);
131
+ else callerSignal?.addEventListener?.('abort', onCallerAbort, { once: true });
132
+ const deadline = watchdog(controller);
133
+ deadline.arm(PROVIDER_FIRST_BYTE_TIMEOUT_MS, 'first byte');
134
+ try {
135
+ const response = await post(controller.signal);
136
+ if (!response.ok) {
137
+ const bodyText = await response.text().catch(() => '');
138
+ throw anthropicQuotaError(response.status, response.headers, bodyText);
139
+ }
140
+ return await readCacheWarmStream(response, {
141
+ onUsage,
142
+ onChunk: (reported) => {
143
+ if (reported) deadline.arm(PROVIDER_SSE_IDLE_TIMEOUT_MS, 'stream');
144
+ },
145
+ });
146
+ } catch (error) {
147
+ // The warm's own error is what the caller needs; a failure to persist the
148
+ // refusal (quota / roster state) is reported, scrubbed, like the other
149
+ // credential and account-state write failures of the Anthropic providers.
150
+ await recordCacheWarmRefusal(providerName, instance, error, {
151
+ accountId,
152
+ model,
153
+ aborted: Boolean(callerSignal?.aborted),
154
+ }).catch((recordError) => {
155
+ process.stderr.write(
156
+ `[anthropic-cache-warm] refusal record failed: ${scrubOAuthSecrets(recordError?.message || String(recordError))}\n`
157
+ );
158
+ });
159
+ throw error;
160
+ } finally {
161
+ callerSignal?.removeEventListener?.('abort', onCallerAbort);
162
+ deadline.clear();
163
+ controller.abort(new Error('cache warm finished'));
164
+ }
165
+ }
@@ -10,7 +10,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
10
10
  // source release just to advance the user-agent version.
11
11
  // Claude Opus 5.5 requires 2.1.280 or newer. The release workflow raises this
12
12
  // to the published CLI version (scripts/sync-client-version-floors.mjs).
13
- export const DEFAULT_CLI_VERSION = '2.1.310';
13
+ export const DEFAULT_CLI_VERSION = '2.1.311';
14
14
  export const CLAUDE_CLI_NPM_PACKAGE = '@anthropic-ai/claude-code';
15
15
 
16
16
  const CACHE_SCHEMA_VERSION = 1;
@@ -15,6 +15,7 @@ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs
15
15
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
16
16
  import { modelProxyClientMode, serverModelCredentialsDisabledError } from '../../../shared/model-proxy-mode.mjs';
17
17
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
18
+ import { TIMEOUT_PHASE } from '../../../shared/provider-timeout-phase.mjs';
18
19
  import { claudeCliUserAgent, resolveCliVersion } from './anthropic-oauth-client-version.mjs';
19
20
  import {
20
21
  accountIdentityFields,
@@ -375,7 +376,9 @@ async function _refreshOAuthCredentialsUnlocked(creds) {
375
376
  return refreshed;
376
377
  } catch (err) {
377
378
  if (err?.name === 'AbortError') {
378
- throw new Error('Anthropic OAuth token refresh timed out after 30000ms');
379
+ throw Object.assign(new Error('Anthropic OAuth token refresh timed out after 30000ms'), {
380
+ timeoutPhase: TIMEOUT_PHASE.AUTH,
381
+ });
379
382
  }
380
383
  throw err;
381
384
  } finally {