@desplega.ai/agent-swarm 1.168.0 → 1.171.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 (410) hide show
  1. package/README.md +11 -2
  2. package/dist/{acp-adapter-zhejfecx.js → acp-adapter-1z9p3fvx.js} +6 -6
  3. package/dist/{actions-7jg840nr.js → actions-rc2yjk8a.js} +41 -33
  4. package/dist/{amp-adapter-ye6rnetm.js → amp-adapter-ypyf5s1x.js} +6 -6
  5. package/dist/{anthropic-messages-13yn502s.js → anthropic-messages-etkbxhhy.js} +11 -11
  6. package/dist/{app-ffes2kyf.js → app-04ab2s0z.js} +10 -10
  7. package/dist/{artifact-fey7dvj9.js → artifact-eczsknc3.js} +6 -3
  8. package/dist/{assistant-fhm5smyz.js → assistant-vhmjd8wf.js} +47 -36
  9. package/dist/{azure-openai-responses-9st34pfz.js → azure-openai-responses-xap57x4b.js} +7 -7
  10. package/dist/{boot-reembed-jvfadhsk.js → boot-reembed-3p0m259x.js} +9 -9
  11. package/dist/{boot-reembed-20q3nzvg.js → boot-reembed-wew3gwk8.js} +9 -9
  12. package/dist/{boot-scrub-sweep-jtfajzve.js → boot-scrub-sweep-5ytwtq24.js} +10 -10
  13. package/dist/{boot-seeds-9qt0j8j2.js → boot-seeds-s8p2xry8.js} +13 -13
  14. package/dist/{claude-adapter-a8znhthz.js → claude-adapter-akrzg6jh.js} +16 -16
  15. package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-kqqqhp2q.js} +3 -3
  16. package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-bnmjarjc.js} +12 -9
  17. package/dist/{claude-sdk-session-7x7chdrs.js → claude-sdk-session-exq1jadx.js} +7818 -7721
  18. package/dist/{cli-g6gx2dn1.js → cli-01qndrvw.js} +17080 -19099
  19. package/dist/{cli-74ez9b66.js → cli-0d2rqhvq.js} +584 -75
  20. package/dist/cli-0p3enk35.js +188 -0
  21. package/dist/cli-1srmzpr3.js +16 -0
  22. package/dist/{cli-6bpvv8h4.js → cli-231av4y6.js} +7 -7
  23. package/dist/{cli-9q0bh411.js → cli-280gnz4v.js} +2 -2
  24. package/dist/{cli-mv2520nm.js → cli-2bw3w47j.js} +1 -1
  25. package/dist/{cli-fqq06kkb.js → cli-2qbs2tvf.js} +22 -14
  26. package/dist/{cli-efnd2kce.js → cli-2rbt7at8.js} +6 -1
  27. package/dist/{cli-xvc3q9br.js → cli-451ks6aw.js} +1 -1
  28. package/dist/{cli-0jngabtb.js → cli-4bm25xtw.js} +2 -2
  29. package/dist/{cli-zp38xvtm.js → cli-5rqrj7q0.js} +1 -1
  30. package/dist/{cli-km4x66j0.js → cli-5y67yrt6.js} +13 -13
  31. package/dist/{cli-nctfv2yy.js → cli-62bwwc2q.js} +8 -8
  32. package/dist/cli-6xxxb8qt.js +2084 -0
  33. package/dist/cli-6zx22mv6.js +0 -0
  34. package/dist/{cli-zp67qg5x.js → cli-7a0sde2x.js} +72 -1
  35. package/dist/{cli-v1w7pbd8.js → cli-7jykv7ef.js} +1 -1
  36. package/dist/{cli-rfphkzh5.js → cli-7pz3b7x5.js} +24 -4
  37. package/dist/{cli-mb75wpxf.js → cli-81r1wjyv.js} +1 -1
  38. package/dist/{cli-svah6b6w.js → cli-8mr695sb.js} +9 -5
  39. package/dist/{cli-f14fvzag.js → cli-a1b9b32n.js} +6 -1
  40. package/dist/cli-cd0hv9nh.js +32 -0
  41. package/dist/{cli-rr5z1j4p.js → cli-ctret352.js} +691 -213
  42. package/dist/{cli-wt7ccy4r.js → cli-d161g69q.js} +9 -1
  43. package/dist/cli-db3m6zdt.js +362 -0
  44. package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
  45. package/dist/{cli-hf5s9pgf.js → cli-e3pmyt39.js} +1 -1
  46. package/dist/{cli-pgehz8e1.js → cli-ec7hrgqp.js} +404 -89
  47. package/dist/{cli-ng1pmdx6.js → cli-erbp6b4g.js} +10 -7
  48. package/dist/{cli-9k7mqz00.js → cli-ge593j7e.js} +30 -3
  49. package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
  50. package/dist/cli-h8w3fg83.js +17 -0
  51. package/dist/{cli-xvdgasgy.js → cli-hsem16hb.js} +114 -9
  52. package/dist/{cli-ytmmyj7f.js → cli-hsk5gd15.js} +9 -5
  53. package/dist/{cli-aj1e0h0d.js → cli-jxsgrnmq.js} +201 -37
  54. package/dist/{cli-tc5zec7v.js → cli-k2v5x13t.js} +2 -2
  55. package/dist/{cli-69egj654.js → cli-m4csbs67.js} +2 -2
  56. package/dist/{cli-43jg9xdw.js → cli-mzj3aqac.js} +3 -3
  57. package/dist/{cli-p5jqk8fr.js → cli-n805bb98.js} +1 -1
  58. package/dist/{cli-ew0c3a1j.js → cli-n8ybnv1k.js} +3 -3
  59. package/dist/{cli-j05x7pnb.js → cli-nkjwe4z7.js} +219 -41
  60. package/dist/{cli-jb931ba2.js → cli-p2pjfq1r.js} +2 -2
  61. package/dist/{cli-ee0402n1.js → cli-p8f7dqgt.js} +871 -2980
  62. package/dist/{cli-r7eeddkq.js → cli-ph3145b7.js} +3 -3
  63. package/dist/cli-ppy4nnnh.js +168 -0
  64. package/dist/cli-r455yd58.js +832 -0
  65. package/dist/{cli-mxvnnv5q.js → cli-rrbctenq.js} +2 -2
  66. package/dist/{cli-47zfzshm.js → cli-s88my3br.js} +40 -28
  67. package/dist/{cli-9q24yyvs.js → cli-srmej5tp.js} +3 -3
  68. package/dist/{cli-bcspch6h.js → cli-tg8q0b5y.js} +14 -11
  69. package/dist/cli-w0zx9ya1.js +1351 -0
  70. package/dist/cli-wfvedq5w.js +2659 -0
  71. package/dist/{cli-ewhwpyrb.js → cli-x207g4w1.js} +1 -1
  72. package/dist/{cli-fhj4y16f.js → cli-x3fy2tgp.js} +2 -2
  73. package/dist/{cli-7mhy3ths.js → cli-x61srfpp.js} +5 -5
  74. package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
  75. package/dist/{cli-jbzbr36n.js → cli-y1j1ggbf.js} +8 -8
  76. package/dist/{cli-jvmtxqtc.js → cli-ywh2a87k.js} +7 -2
  77. package/dist/{cli-3qhyh31j.js → cli-ywm1azg9.js} +6 -0
  78. package/dist/{cli-z48ggfyq.js → cli-zxm9p3x4.js} +2 -2
  79. package/dist/cli.js +49 -28
  80. package/dist/{codex-adapter-n63k33d3.js → codex-adapter-9ag75whm.js} +13 -13
  81. package/dist/{codex-hook-15dgc4cb.js → codex-hook-7ba15hjd.js} +7 -5
  82. package/dist/{codex-login-g1cyt5zm.js → codex-login-841kv5fk.js} +7 -5
  83. package/dist/{codex-session-runner-xkxfd81a.js → codex-session-runner-9fp7y4cf.js} +13 -13
  84. package/dist/{commands-q4mfhn51.js → commands-d7eah0y9.js} +11 -11
  85. package/dist/controller-hq5ggpcp.js +193 -0
  86. package/dist/controllers-connect-8t0k65x2.js +265 -0
  87. package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-t4pr1m3c.js} +6 -6
  88. package/dist/db-2rmd23wq.js +71 -0
  89. package/dist/{db-1t0bj3pc.js → db-sp39qv72.js} +9 -9
  90. package/dist/{devin-adapter-bffjj5n4.js → devin-adapter-xgtj680j.js} +2 -2
  91. package/dist/{dispatcher-k1fhavcf.js → dispatcher-g7fsybsd.js} +32 -28
  92. package/dist/docker-f503k9p0.js +201 -0
  93. package/dist/{dsh-adapter-5423p70m.js → dsh-adapter-2h8m4cab.js} +5 -5
  94. package/dist/e2b-66xra4az.js +202 -0
  95. package/dist/{e2b-nd50eccg.js → e2b-xwhyj8cw.js} +43 -527
  96. package/dist/{event-bus-vnfthafg.js → event-bus-vjtv82gm.js} +2 -2
  97. package/dist/{grok-adapter-phrndeb8.js → grok-adapter-1err1me7.js} +7 -7
  98. package/dist/{handlers-59ywqwzh.js → handlers-c7yggm37.js} +35 -31
  99. package/dist/heartbeat-dkm924fk.js +124 -0
  100. package/dist/{hook-atj0swc2.js → hook-g8e7sfnf.js} +14 -14
  101. package/dist/{hook-mew1chq9.js → hook-wgx4exw6.js} +21 -18
  102. package/dist/{http-ef2fnn9v.js → http-1d23k928.js} +11499 -9018
  103. package/dist/{index-9fc2h7ww.js → index-gm10tmye.js} +32 -28
  104. package/dist/{index-w2yhye23.js → index-q7cj9bn2.js} +41 -29
  105. package/dist/{index-48pb9erw.js → index-va4zbvzp.js} +42 -37
  106. package/dist/{index-gr69c43q.js → index-vertaxs1.js} +9 -9
  107. package/dist/{index-h26wjcfy.js → index-y5vvczrs.js} +62 -24
  108. package/dist/{keepalive-qhcqz56v.js → keepalive-1jc5e7ct.js} +11 -11
  109. package/dist/{lead-hpk5r3b1.js → lead-yxw2f1sg.js} +52 -48
  110. package/dist/{maintenance-nvkyp2xe.js → maintenance-h7w2m10y.js} +9 -9
  111. package/dist/{node-8zkdq42h.js → node-c9we95xf.js} +9 -9
  112. package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-ngskthwh.js → oauth-refresh-sweep-wh9ypd1b.js} +10 -10
  114. package/dist/{onboard-yfnkqhxc.js → onboard-dfr9j762.js} +25 -15
  115. package/dist/{openai-codex-responses-jzxkta9b.js → openai-codex-responses-pgvtddvv.js} +3 -3
  116. package/dist/{openai-responses-m3g43rrt.js → openai-responses-9dyzwzkr.js} +9 -9
  117. package/dist/{opencode-adapter-yjyqynzf.js → opencode-adapter-f1zby94p.js} +3 -3
  118. package/dist/{otel-impl-hhm64r6m.js → otel-impl-and31m5s.js} +5 -5
  119. package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-fx6k5wwh.js} +40 -28
  120. package/dist/{pricing-refresh-35f91nx2.js → pricing-refresh-23zr6j3w.js} +11 -11
  121. package/dist/{rbac-roles-97w370qj.js → rbac-roles-nc7k9d0t.js} +11 -11
  122. package/dist/{rbac-roles-z6sje3gy.js → rbac-roles-wnx35x4x.js} +12 -12
  123. package/dist/{render-v2-n4v66bj3.js → render-v2-81cg1y52.js} +10 -10
  124. package/dist/resume-d4b6j2n2.js +70 -0
  125. package/dist/{setup-m3hhyv5f.js → setup-g68r1w28.js} +7 -5
  126. package/dist/static-t1gr2cr7.js +30 -0
  127. package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-rdjmcg7q.js} +3 -3
  128. package/dist/{work-objects-mxf2c842.js → work-objects-0zzdnbt9.js} +37 -33
  129. package/dist/{worker-9t07z1gq.js → worker-p1g8qm6h.js} +52 -48
  130. package/dist/{x-ma4gnfgy.js → x-qfmygqqv.js} +2 -2
  131. package/openapi.json +15455 -11731
  132. package/package.json +10 -7
  133. package/src/apps/store.ts +1 -0
  134. package/src/artifact-sdk/feedback-overlay.ts +528 -0
  135. package/src/be/db/agents.ts +6 -0
  136. package/src/be/db/instance-signals.ts +30 -0
  137. package/src/be/db/tasks/read.ts +2 -0
  138. package/src/be/db/tasks/write.ts +39 -5
  139. package/src/be/db/workflow-runs.ts +117 -0
  140. package/src/be/db-queries/slack-inbound.ts +489 -0
  141. package/src/be/db.ts +105 -42
  142. package/src/be/extensions/assets.ts +1 -1
  143. package/src/be/fleet-benchmark-stats.ts +39 -0
  144. package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
  145. package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
  146. package/src/be/migrations/205_favorites_per_dashboard_user.sql +29 -0
  147. package/src/be/migrations/206_runtime_pools.sql +62 -0
  148. package/src/be/migrations/207_runtime_session_tokens.sql +31 -0
  149. package/src/be/migrations/208_task_runtime_dispatch.sql +4 -0
  150. package/src/be/model-tier-resolution.ts +9 -2
  151. package/src/be/multi-runtime.ts +102 -13
  152. package/src/be/org-profile.ts +35 -0
  153. package/src/be/rbac-audit.ts +4 -0
  154. package/src/be/runtime-pools/controllers.ts +150 -0
  155. package/src/be/runtime-pools/db.ts +526 -0
  156. package/src/be/runtime-pools/demand.ts +65 -0
  157. package/src/be/runtime-pools/e2b-images.ts +109 -0
  158. package/src/be/runtime-pools/errors.ts +8 -0
  159. package/src/be/runtime-pools/lifecycle.ts +44 -0
  160. package/src/be/runtime-pools/reachability.ts +30 -0
  161. package/src/be/runtime-pools/recovery.ts +248 -0
  162. package/src/be/runtime-pools/remote-controller.ts +144 -0
  163. package/src/be/runtime-pools/routing.ts +304 -0
  164. package/src/be/runtime-pools/seed.ts +110 -0
  165. package/src/be/runtime-pools/supervisor.ts +981 -0
  166. package/src/be/runtime-pools/tokens.ts +57 -0
  167. package/src/be/script-connections.ts +1 -1
  168. package/src/be/scripts/run-saved.ts +40 -20
  169. package/src/be/scripts/typecheck.ts +48 -3
  170. package/src/be/secret-registry.ts +17 -1
  171. package/src/be/skill-usage.ts +467 -0
  172. package/src/be/swarm-config-guard.ts +62 -2
  173. package/src/be/telemetry-enable.ts +66 -0
  174. package/src/be/telemetry-ingest-anchor.ts +77 -0
  175. package/src/be/users.ts +173 -23
  176. package/src/be/worker-nudge-listener.ts +28 -0
  177. package/src/be/worker-nudges.ts +22 -0
  178. package/src/cli.tsx +22 -3
  179. package/src/commands/controller.ts +180 -0
  180. package/src/commands/onboard/non-interactive.ts +36 -12
  181. package/src/commands/onboard/types.ts +1 -1
  182. package/src/commands/onboard.tsx +3 -0
  183. package/src/commands/push-delivery.ts +230 -0
  184. package/src/commands/runner.ts +560 -276
  185. package/src/e2b/dispatch.ts +15 -1
  186. package/src/extensions/catalog.ts +1 -1
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/dispatcher.ts +3 -3
  189. package/src/fleet-benchmark.ts +157 -0
  190. package/src/heartbeat/heartbeat.ts +205 -137
  191. package/src/heartbeat/templates.ts +16 -0
  192. package/src/hooks/hook.ts +3 -2
  193. package/src/http/agents.ts +21 -0
  194. package/src/http/all-routes.ts +7 -0
  195. package/src/http/apps.ts +4 -2
  196. package/src/http/auth.ts +4 -7
  197. package/src/http/config.ts +58 -21
  198. package/src/http/controllers-connect.ts +206 -0
  199. package/src/http/core.ts +108 -14
  200. package/src/http/favorite-owner.ts +31 -1
  201. package/src/http/fleet-benchmark.ts +206 -0
  202. package/src/http/index.ts +40 -6
  203. package/src/http/integrations.ts +3 -1
  204. package/src/http/mcp-bridge.ts +1 -0
  205. package/src/http/mcp.ts +3 -8
  206. package/src/http/pages-public.ts +7 -2
  207. package/src/http/pages.ts +178 -0
  208. package/src/http/poll.ts +31 -2
  209. package/src/http/realtime.ts +1 -1
  210. package/src/http/request-principal.ts +2 -0
  211. package/src/http/runtime-pools.ts +527 -0
  212. package/src/http/session-data.ts +2 -0
  213. package/src/http/sessions.ts +100 -23
  214. package/src/http/skills.ts +173 -0
  215. package/src/http/slack-inbound.ts +107 -0
  216. package/src/http/status.ts +25 -1
  217. package/src/http/tasks.ts +59 -28
  218. package/src/http/telemetry-ui.ts +102 -0
  219. package/src/http/used-by.ts +140 -0
  220. package/src/http/users.ts +1 -1
  221. package/src/http/utils.ts +7 -1
  222. package/src/http/webhooks.ts +20 -20
  223. package/src/http/workflows.ts +10 -2
  224. package/src/jira/app.ts +1 -2
  225. package/src/kv-reserved-namespaces.ts +1 -0
  226. package/src/linear/app.ts +1 -2
  227. package/src/prompts/session-templates.ts +32 -0
  228. package/src/providers/acp-target-catalog.ts +75 -1
  229. package/src/providers/acp-targets.ts +115 -1
  230. package/src/providers/amp-adapter.ts +2 -2
  231. package/src/providers/claude-adapter.ts +1 -1
  232. package/src/providers/claude-managed-adapter.ts +2 -11
  233. package/src/providers/claude-sdk-session.ts +1 -1
  234. package/src/providers/codex-adapter.ts +5 -1
  235. package/src/providers/opencode-adapter.ts +3 -3
  236. package/src/providers/pi-mono-adapter.ts +44 -29
  237. package/src/providers/pricing-sources.md +2 -0
  238. package/src/providers/skill-invoke.ts +39 -3
  239. package/src/rbac/legacy-policy.ts +8 -0
  240. package/src/rbac/permissions.ts +16 -0
  241. package/src/rbac/types.ts +2 -0
  242. package/src/realtime/auth.ts +13 -0
  243. package/src/realtime/browser.generated.txt +8 -8
  244. package/src/realtime/browser.ts +3 -3
  245. package/src/realtime/channels.ts +24 -0
  246. package/src/realtime/transport.ts +94 -15
  247. package/src/realtime/worker-client.ts +176 -0
  248. package/src/runtime-providers/controller-protocol.ts +97 -0
  249. package/src/runtime-providers/controller.ts +86 -0
  250. package/src/runtime-providers/docker.ts +235 -0
  251. package/src/runtime-providers/e2b.ts +217 -0
  252. package/src/runtime-providers/index.ts +37 -0
  253. package/src/runtime-providers/static.ts +26 -0
  254. package/src/runtime-providers/types.ts +92 -0
  255. package/src/runtime-providers/worker-api-url.ts +11 -0
  256. package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
  257. package/src/scripts-runtime/executors/quickjs.ts +9 -2
  258. package/src/scripts-runtime/sdk-allowlist.ts +6 -0
  259. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  260. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  261. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  262. package/src/server.ts +13 -0
  263. package/src/slack/actions.ts +6 -2
  264. package/src/slack/app.ts +5 -0
  265. package/src/slack/assistant.ts +15 -5
  266. package/src/slack/egress-scrub.ts +3 -2
  267. package/src/slack/handlers.ts +36 -18
  268. package/src/slack/inbound-dispatch.ts +549 -0
  269. package/src/slack/inbound-files.ts +2 -0
  270. package/src/slack/router.ts +7 -5
  271. package/src/tasks/worker-follow-up.ts +80 -52
  272. package/src/telemetry-context.ts +7 -0
  273. package/src/telemetry-contract/EVENTS.md +14 -1
  274. package/src/telemetry-contract/SOURCE.json +4 -4
  275. package/src/telemetry-contract/catalog.json +155 -0
  276. package/src/telemetry-contract/types.gen.ts +248 -4
  277. package/src/telemetry-ui-events.ts +130 -0
  278. package/src/telemetry.ts +43 -3
  279. package/src/tests/acp-adapter.test.ts +129 -0
  280. package/src/tests/claude-adapter.test.ts +6 -2
  281. package/src/tests/codex-adapter.test.ts +6 -2
  282. package/src/tests/codex-oauth-device.test.ts +1 -1
  283. package/src/tests/config-internal-keys.test.ts +27 -0
  284. package/src/tests/config-session-auth.test.ts +19 -3
  285. package/src/tests/db-retention.test.ts +50 -10
  286. package/src/tests/docs-index-footprint.test.js +3 -3
  287. package/src/tests/e2b-dispatch.test.ts +28 -1
  288. package/src/tests/e2e-runtime-pools.test.ts +51 -0
  289. package/src/tests/entrypoint-acp-target-binary.test.ts +38 -1
  290. package/src/tests/favorites.test.ts +188 -0
  291. package/src/tests/fetch-resolved-env.test.ts +59 -1
  292. package/src/tests/fleet-benchmark.test.ts +453 -0
  293. package/src/tests/harness-model-support.test.ts +70 -2
  294. package/src/tests/heartbeat-checklist.test.ts +243 -0
  295. package/src/tests/heartbeat-orphan-resume.test.ts +24 -0
  296. package/src/tests/heartbeat-reboot-live.test.ts +31 -0
  297. package/src/tests/heartbeat-supersede-resume.test.ts +21 -0
  298. package/src/tests/hook-runtime-close.test.ts +64 -0
  299. package/src/tests/http-task-write-ownership.test.ts +8 -1
  300. package/src/tests/integration-disable-flags.test.ts +45 -0
  301. package/src/tests/migration-206-runtime-pools.test.ts +134 -0
  302. package/src/tests/migration-207-runtime-session-tokens.test.ts +45 -0
  303. package/src/tests/migration-208-task-runtime-dispatch.test.ts +30 -0
  304. package/src/tests/multi-runtime-registration.test.ts +219 -4
  305. package/src/tests/onboard-non-interactive.test.ts +104 -6
  306. package/src/tests/opencode-adapter.test.ts +6 -2
  307. package/src/tests/org-website.test.ts +115 -0
  308. package/src/tests/pages-feedback.test.ts +218 -0
  309. package/src/tests/pi-mono-adapter.test.ts +74 -10
  310. package/src/tests/rbac-admission-e2e.test.ts +4 -4
  311. package/src/tests/rbac-engine.test.ts +8 -1
  312. package/src/tests/realtime-room-auth.test.ts +49 -0
  313. package/src/tests/repo-allow-merge-gate.test.ts +8 -1
  314. package/src/tests/runner-exit-after-tasks.test.ts +163 -0
  315. package/src/tests/runner-finish-retry.test.ts +466 -0
  316. package/src/tests/runner-harness-telemetry-env.test.ts +105 -0
  317. package/src/tests/runner-push-delivery.test.ts +670 -0
  318. package/src/tests/runner-slot-reconcile.test.ts +89 -1
  319. package/src/tests/runtime-child-token-cleanup.test.ts +172 -0
  320. package/src/tests/runtime-controller-token.test.ts +254 -0
  321. package/src/tests/runtime-demand.test.ts +119 -0
  322. package/src/tests/runtime-e2b-pools.test.ts +847 -0
  323. package/src/tests/runtime-pool-reachability.test.ts +328 -0
  324. package/src/tests/runtime-pool-routing.test.ts +817 -0
  325. package/src/tests/runtime-pools-db.test.ts +275 -0
  326. package/src/tests/runtime-pools-http.test.ts +506 -0
  327. package/src/tests/runtime-pools-ownership-guards.test.ts +235 -0
  328. package/src/tests/runtime-pools-seed.test.ts +203 -0
  329. package/src/tests/runtime-pools-test-utils.ts +128 -0
  330. package/src/tests/runtime-provider-registration.test.ts +133 -0
  331. package/src/tests/runtime-providers-docker.test.ts +400 -0
  332. package/src/tests/runtime-providers-e2b-transport.test.ts +50 -0
  333. package/src/tests/runtime-providers-e2b.test.ts +335 -0
  334. package/src/tests/runtime-recovery.test.ts +745 -0
  335. package/src/tests/runtime-remote-controller.test.ts +846 -0
  336. package/src/tests/runtime-supervisor-failures.test.ts +387 -0
  337. package/src/tests/runtime-supervisor.test.ts +1039 -0
  338. package/src/tests/runtime-token-auth.test.ts +897 -0
  339. package/src/tests/script-connections.test.ts +9 -0
  340. package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
  341. package/src/tests/script-executor-registry.test.ts +38 -0
  342. package/src/tests/secret-scrubber.test.ts +17 -2
  343. package/src/tests/session-costs-codex-recompute.test.ts +49 -0
  344. package/src/tests/session-tokens.test.ts +13 -3
  345. package/src/tests/skill-invoke.test.ts +142 -0
  346. package/src/tests/skill-usage-cohorts.test.ts +232 -0
  347. package/src/tests/skill-usage.test.ts +348 -0
  348. package/src/tests/slack-attachments-block.test.ts +23 -0
  349. package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
  350. package/src/tests/slack-inbound-receipts.test.ts +684 -0
  351. package/src/tests/slack-render-v2.test.ts +6 -0
  352. package/src/tests/slack-session-status.test.ts +4 -2
  353. package/src/tests/status-instance.test.ts +71 -0
  354. package/src/tests/task-supersede-resume.test.ts +87 -4
  355. package/src/tests/telemetry-enable.test.ts +113 -0
  356. package/src/tests/telemetry-ui-route.test.ts +195 -0
  357. package/src/tests/tool-annotations.test.ts +1 -1
  358. package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
  359. package/src/tests/used-by-route.test.ts +237 -0
  360. package/src/tests/user-token-routes.test.ts +1 -1
  361. package/src/tests/worker-nudges.test.ts +653 -0
  362. package/src/tests/worker-realtime-client.test.ts +188 -0
  363. package/src/tests/workflow-executors.test.ts +3 -2
  364. package/src/tests/workflow-sub-workflow.test.ts +385 -0
  365. package/src/tools/app-upsert.ts +1 -1
  366. package/src/tools/runtime-controller-list.ts +28 -0
  367. package/src/tools/runtime-pool-bind-agent.ts +29 -0
  368. package/src/tools/runtime-pool-common.ts +37 -0
  369. package/src/tools/runtime-pool-create-agent.ts +29 -0
  370. package/src/tools/runtime-pool-delete.ts +27 -0
  371. package/src/tools/runtime-pool-list.ts +27 -0
  372. package/src/tools/runtime-pool-upsert.ts +31 -0
  373. package/src/tools/script-common.ts +1 -1
  374. package/src/tools/send-task.ts +3 -2
  375. package/src/tools/tool-config.ts +6 -0
  376. package/src/types.ts +137 -2
  377. package/src/utils/config-secrets.ts +1 -0
  378. package/src/utils/internal-ai/complete-structured.ts +6 -3
  379. package/src/utils/lead-monitor-channels.ts +3 -0
  380. package/src/utils/org-website.ts +104 -0
  381. package/src/utils/request-auth-context.ts +10 -2
  382. package/src/utils/runtime-model-catalog.ts +1 -1
  383. package/src/utils/runtime-pools-flag.ts +11 -0
  384. package/src/utils/secret-scrubber.ts +9 -0
  385. package/src/utils/session-token-connections.ts +20 -0
  386. package/src/workflows/engine.ts +17 -1
  387. package/src/workflows/executors/agent-task.ts +2 -2
  388. package/src/workflows/executors/human-in-the-loop.ts +3 -3
  389. package/src/workflows/executors/registry.ts +2 -0
  390. package/src/workflows/executors/script.ts +1 -1
  391. package/src/workflows/executors/sub-workflow.ts +135 -0
  392. package/src/workflows/executors/wait.ts +3 -3
  393. package/src/workflows/recovery.ts +11 -1
  394. package/src/workflows/resume.ts +77 -0
  395. package/src/workflows/wait-filter.ts +5 -7
  396. package/templates/skills/apps/content.md +1 -1
  397. package/templates/skills/code-quality/content.md +9 -1
  398. package/templates/skills/db-query-guidance/content.md +9 -0
  399. package/templates/skills/heartbeat-runbook/content.md +3 -1
  400. package/templates/skills/pages/content.md +25 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  402. package/templates/skills/swarm-scripts/content.md +1 -1
  403. package/templates/skills/work-on-task/SKILL.md +1 -1
  404. package/templates/skills/work-on-task/content.md +1 -1
  405. package/dist/{cli-qd97v6z2.js → cli-1qtgd1h1.js} +3 -3
  406. package/dist/{cli-cmxkbzzs.js → cli-j0w59p15.js} +6 -6
  407. package/dist/{google-generative-ai-b8zxrpax.js → google-generative-ai-pvhm0dq1.js} +1 -1
  408. package/dist/{google-vertex-nrdd697t.js → google-vertex-tek9sqe2.js} +4 -4
  409. package/dist/{mistral-conversations-059y51zt.js → mistral-conversations-rjxa9s1j.js} +3 -3
  410. package/dist/{openai-completions-663yncfq.js → openai-completions-44gb0zsk.js} +12 -12
@@ -0,0 +1,817 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
11
+ import {
12
+ assignUnassignedTaskPending,
13
+ closeDb,
14
+ createAgent,
15
+ createTaskExtended,
16
+ createUser,
17
+ getDbClient,
18
+ getTaskById,
19
+ initDb,
20
+ MAX_EMPTY_POLLS,
21
+ rejectTask,
22
+ releaseTask,
23
+ startTask,
24
+ updateAgentProfile,
25
+ upsertBudget,
26
+ } from "../be/db";
27
+ import { isInTransaction } from "../be/db-client";
28
+ import { upsertRuntimeInstance } from "../be/multi-runtime";
29
+ import {
30
+ createPoolAgent,
31
+ createRuntimeController,
32
+ createRuntimePool,
33
+ updateRuntimePool,
34
+ } from "../be/runtime-pools/db";
35
+ import { routePoolTasks, startPoolRouting, stopPoolRouting } from "../be/runtime-pools/routing";
36
+ import { validateConfigValue } from "../be/swarm-config-guard";
37
+ import { codeLevelTriage } from "../heartbeat/heartbeat";
38
+ import type { Agent, RuntimePool } from "../types";
39
+ import { workflowEventBus } from "../workflows/event-bus";
40
+
41
+ const dbPath = "./test-runtime-pool-routing.sqlite";
42
+ const envKeys = [
43
+ "RUNTIME_POOLS_ENABLED",
44
+ "RUNTIME_WAKE_GRACE_SEC",
45
+ "RUNTIME_WAKE_REROUTE_SEC",
46
+ "RUNTIME_ROUTING_INTERVAL_MS",
47
+ "MULTI_RUNTIME_ENABLED",
48
+ "POOL_AFFINITY_ENFORCEMENT",
49
+ "BUDGET_ADMISSION_DISABLED",
50
+ "HEARTBEAT_MAX_AUTO_ASSIGN",
51
+ ];
52
+ const savedEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
53
+ let pool: RuntimePool;
54
+ async function cleanup() {
55
+ for (const suffix of ["", "-wal", "-shm"])
56
+ await Bun.file(dbPath + suffix)
57
+ .delete()
58
+ .catch(() => {});
59
+ }
60
+ beforeAll(async () => {
61
+ await cleanup();
62
+ closeDb();
63
+ initDb(dbPath);
64
+ });
65
+ afterAll(async () => {
66
+ stopPoolRouting();
67
+ closeDb();
68
+ await cleanup();
69
+ for (const key of envKeys) {
70
+ if (savedEnv[key] === undefined) delete process.env[key];
71
+ else process.env[key] = savedEnv[key];
72
+ }
73
+ });
74
+ afterEach(() => stopPoolRouting());
75
+ beforeEach(async () => {
76
+ process.env.RUNTIME_POOLS_ENABLED = "true";
77
+ process.env.RUNTIME_WAKE_GRACE_SEC = "15";
78
+ process.env.RUNTIME_WAKE_REROUTE_SEC = "600";
79
+ process.env.RUNTIME_ROUTING_INTERVAL_MS = "60000";
80
+ process.env.MULTI_RUNTIME_ENABLED = "true";
81
+ process.env.POOL_AFFINITY_ENFORCEMENT = "true";
82
+ process.env.BUDGET_ADMISSION_DISABLED = "false";
83
+ process.env.HEARTBEAT_MAX_AUTO_ASSIGN = "5";
84
+ for (const table of [
85
+ "active_sessions",
86
+ "agent_tasks",
87
+ "runtime_instances",
88
+ "agents",
89
+ "runtime_pools",
90
+ "runtime_controllers",
91
+ "budgets",
92
+ ])
93
+ await getDbClient().run(`DELETE FROM ${table}`);
94
+ const controller = await createRuntimeController({ name: "docker", kind: "docker" });
95
+ pool = await createRuntimePool({
96
+ name: "coders",
97
+ kind: "docker",
98
+ placements: [controller.id],
99
+ image: "test-worker",
100
+ });
101
+ });
102
+ async function sleeper(name = "sleeping", maxTasks = 1) {
103
+ return await createPoolAgent(pool.id, { name, maxTasks });
104
+ }
105
+ async function live(status: Agent["status"] = "idle", maxTasks = 1) {
106
+ const agent = await createAgent({ name: "live", isLead: false, status, maxTasks });
107
+ await upsertRuntimeInstance({
108
+ id: crypto.randomUUID(),
109
+ agentId: agent.id,
110
+ reportedSlots: maxTasks,
111
+ });
112
+ return agent;
113
+ }
114
+ function afterGrace(task: { createdAt: string }) {
115
+ return new Date(Date.parse(task.createdAt) + 16000);
116
+ }
117
+ async function waitForAssignment(id: string, agentId: string) {
118
+ const deadline = Date.now() + 2000;
119
+ while (Date.now() < deadline) {
120
+ const task = await getTaskById(id);
121
+ if (task?.status === "pending" && task.agentId === agentId) return;
122
+ await Bun.sleep(10);
123
+ }
124
+ expect(await getTaskById(id)).toMatchObject({ status: "pending", agentId });
125
+ }
126
+
127
+ async function createAfterEventDelivery(options: Parameters<typeof createTaskExtended>[1] = {}) {
128
+ const emitted = Promise.withResolvers<void>();
129
+ const onCreated = () => emitted.resolve();
130
+ workflowEventBus.on("task.created", onCreated);
131
+ try {
132
+ const task = await createTaskExtended("event sentinel", options);
133
+ await emitted.promise;
134
+ return task;
135
+ } finally {
136
+ workflowEventBus.off("task.created", onCreated);
137
+ }
138
+ }
139
+
140
+ describe("server pool routing", () => {
141
+ test.each([
142
+ "RUNTIME_WAKE_GRACE_SEC",
143
+ "RUNTIME_WAKE_REROUTE_SEC",
144
+ "RUNTIME_ROUTING_INTERVAL_MS",
145
+ ])("%s validates nonnegative integers", (key) => {
146
+ for (const value of ["0", "5000"]) expect(validateConfigValue(key, value)).toBeNull();
147
+ for (const value of ["-1", "0.5", "invalid"])
148
+ expect(validateConfigValue(key, value)).not.toBeNull();
149
+ });
150
+ test.each(["release", "reject"])("flag off suppresses %s routing events", async (action) => {
151
+ process.env.RUNTIME_POOLS_ENABLED = "false";
152
+ const holder = await sleeper();
153
+ const task = await createAfterEventDelivery(
154
+ action === "release" ? { agentId: holder.id } : { offeredTo: holder.id },
155
+ );
156
+ const events = spyOn(workflowEventBus, "emit");
157
+ try {
158
+ if (action === "release") await releaseTask(task.id);
159
+ else await rejectTask(task.id, holder.id);
160
+ // A creation event proves preceding post-commit event callbacks have completed.
161
+ await createAfterEventDelivery({ offeredTo: holder.id });
162
+ expect(events.mock.calls.filter(([event]) => event === "task.unassigned")).toHaveLength(0);
163
+ } finally {
164
+ events.mockRestore();
165
+ }
166
+ });
167
+ test("assignment CAS rejects changed snapshots", async () => {
168
+ const agent = await sleeper();
169
+ const task = await createTaskExtended("pool work");
170
+ await updateAgentProfile(agent.id, { role: "changed" });
171
+ await getDbClient().run("UPDATE agents SET lastUpdatedAt = ? WHERE id = ?", [
172
+ "changed",
173
+ agent.id,
174
+ ]);
175
+ expect(
176
+ await assignUnassignedTaskPending(task.id, agent.id, { routingSnapshot: { task, agent } }),
177
+ ).toBeNull();
178
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
179
+ });
180
+ test("wake release CAS preserves work claimed after its snapshot", async () => {
181
+ const agent = await sleeper();
182
+ const task = await createTaskExtended("pool work");
183
+ await routePoolTasks({ taskIds: [task.id], now: afterGrace(task) });
184
+ const snapshot = await getTaskById(task.id);
185
+ expect(snapshot?.agentId).toBe(agent.id);
186
+ await startTask(task.id);
187
+ expect(await releaseTask(task.id, snapshot!)).toBeNull();
188
+ expect((await getTaskById(task.id))?.status).toBe("in_progress");
189
+ });
190
+ test("pools require multi-runtime and warn once at startup", async () => {
191
+ process.env.MULTI_RUNTIME_ENABLED = "false";
192
+ await sleeper();
193
+ await live("busy", 2);
194
+ const task = await createTaskExtended("pool work");
195
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([]);
196
+ const idle = await createAgent({ name: "legacy", isLead: false, status: "idle" });
197
+ expect(await routePoolTasks()).toEqual([{ taskId: task.id, agentId: idle.id }]);
198
+ const warning = spyOn(console, "warn").mockImplementation(() => {});
199
+ try {
200
+ startPoolRouting();
201
+ startPoolRouting();
202
+ expect(warning).toHaveBeenCalledTimes(1);
203
+ expect(warning.mock.calls[0]?.[0]).toContain("MULTI_RUNTIME_ENABLED");
204
+ } finally {
205
+ warning.mockRestore();
206
+ }
207
+ });
208
+ test("listeners route when the flag is enabled after startup", async () => {
209
+ process.env.RUNTIME_POOLS_ENABLED = "false";
210
+ const warm = await live();
211
+ startPoolRouting();
212
+ process.env.RUNTIME_POOLS_ENABLED = "true";
213
+ const task = await createTaskExtended("enabled later");
214
+ await waitForAssignment(task.id, warm.id);
215
+ });
216
+ test.each([
217
+ ["0", 5000],
218
+ ["-1", 5000],
219
+ ["20", 500],
220
+ ["750", 750],
221
+ ] as const)("interval %s uses %d milliseconds", (configured, expected) => {
222
+ process.env.RUNTIME_ROUTING_INTERVAL_MS = configured;
223
+ const interval = spyOn(globalThis, "setInterval");
224
+ try {
225
+ startPoolRouting();
226
+ expect(interval.mock.calls[0]?.[1]).toBe(expected);
227
+ } finally {
228
+ stopPoolRouting();
229
+ interval.mockRestore();
230
+ }
231
+ });
232
+ test("targeted routing reads snapshots outside a write transaction and skips releases", async () => {
233
+ const sleeping = await sleeper("sleeping", 2);
234
+ const held = await createTaskExtended("wake");
235
+ const at = afterGrace(held);
236
+ await routePoolTasks({ taskIds: [held.id], now: at });
237
+ const task = await createTaskExtended("targeted");
238
+ const client = getDbClient();
239
+ const query = client.query.bind(client);
240
+ const reads = spyOn(client, "query").mockImplementation(async (sql, params) => {
241
+ if (isInTransaction()) expect(sql).toContain("SELECT r.agent_id");
242
+ expect(sql).not.toContain("pool_wake_at IS NOT NULL");
243
+ return await query(sql, params);
244
+ });
245
+ try {
246
+ await routePoolTasks({ taskIds: [task.id], now: new Date(at.getTime() + 600001) });
247
+ expect(await getTaskById(held.id)).toMatchObject({
248
+ agentId: sleeping.id,
249
+ poolWakeAt: at.toISOString(),
250
+ });
251
+ expect(reads).toHaveBeenCalled();
252
+ } finally {
253
+ reads.mockRestore();
254
+ }
255
+ });
256
+ test("tier 1 wins over a wakeable agent after grace", async () => {
257
+ await sleeper();
258
+ const warm = await live();
259
+ const task = await createTaskExtended("pool work");
260
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([
261
+ { taskId: task.id, agentId: warm.id },
262
+ ]);
263
+ expect((await getTaskById(task.id))?.poolWakeAt).toBeUndefined();
264
+ });
265
+ test.each([
266
+ "disabled",
267
+ "draining",
268
+ "credentials",
269
+ "offline",
270
+ "stale",
271
+ ])("tier 1 rejects a %s runtime or pool and marks the alternative wake", async (reason) => {
272
+ const first = await sleeper("A");
273
+ const runtime = await upsertRuntimeInstance({
274
+ id: crypto.randomUUID(),
275
+ agentId: first.id,
276
+ reportedSlots: 1,
277
+ });
278
+ await getDbClient().run("UPDATE agents SET status = 'idle' WHERE id = ?", [first.id]);
279
+ if (reason === "disabled") await updateRuntimePool(pool.id, { enabled: false });
280
+ if (reason === "draining")
281
+ await getDbClient().run("UPDATE runtime_instances SET draining = 1 WHERE id = ?", [
282
+ runtime!.id,
283
+ ]);
284
+ if (reason === "credentials")
285
+ await getDbClient().run("UPDATE runtime_instances SET credential_ready = 0 WHERE id = ?", [
286
+ runtime!.id,
287
+ ]);
288
+ if (reason === "offline")
289
+ await getDbClient().run("UPDATE runtime_instances SET status = 'offline' WHERE id = ?", [
290
+ runtime!.id,
291
+ ]);
292
+ if (reason === "stale")
293
+ await getDbClient().run("UPDATE runtime_instances SET last_seen_at = ? WHERE id = ?", [
294
+ new Date(Date.now() - 3600000).toISOString(),
295
+ runtime!.id,
296
+ ]);
297
+ const alternativePool = await createRuntimePool({
298
+ name: "alternative",
299
+ kind: "docker",
300
+ placements: pool.placements,
301
+ image: "test-worker",
302
+ });
303
+ const second = await createPoolAgent(alternativePool.id, { name: "B", maxTasks: 2 });
304
+ const task = await createTaskExtended("pool work");
305
+ const now = afterGrace(task);
306
+ await routePoolTasks({ now });
307
+ expect(await getTaskById(task.id)).toMatchObject({
308
+ status: "pending",
309
+ agentId: second.id,
310
+ poolWakeAt: now.toISOString(),
311
+ });
312
+ });
313
+ test("tier 1 reserves physical runtime slots across pending assignments", async () => {
314
+ const warm = await live("idle", 3);
315
+ await getDbClient().run("UPDATE runtime_instances SET reported_slots = 1 WHERE agent_id = ?", [
316
+ warm.id,
317
+ ]);
318
+ const first = await createTaskExtended("first");
319
+ const second = await createTaskExtended("second");
320
+ expect(await routePoolTasks()).toEqual([{ taskId: first.id, agentId: warm.id }]);
321
+ expect((await getTaskById(second.id))?.status).toBe("unassigned");
322
+ });
323
+ test("concurrent tier 1 routing reserves the last physical slot once", async () => {
324
+ const warm = await live("idle", 3);
325
+ await getDbClient().run("UPDATE runtime_instances SET reported_slots = 1 WHERE agent_id = ?", [
326
+ warm.id,
327
+ ]);
328
+ const first = await createTaskExtended("first");
329
+ const second = await createTaskExtended("second");
330
+ const assignments = await Promise.all([
331
+ routePoolTasks({ taskIds: [first.id] }),
332
+ routePoolTasks({ taskIds: [second.id] }),
333
+ ]);
334
+ expect(assignments.flat()).toHaveLength(1);
335
+ });
336
+ test("tier 1 requires an unoccupied runtime slot despite logical headroom", async () => {
337
+ const warm = await live("busy", 3);
338
+ const runtime = await getDbClient().get<{ id: string }>(
339
+ "SELECT id FROM runtime_instances WHERE agent_id = ?",
340
+ [warm.id],
341
+ );
342
+ await getDbClient().run("UPDATE runtime_instances SET reported_slots = 1 WHERE id = ?", [
343
+ runtime!.id,
344
+ ]);
345
+ const active = await createTaskExtended("active", { agentId: warm.id });
346
+ await startTask(active.id);
347
+ await getDbClient().run(
348
+ "INSERT INTO active_sessions (id, agentId, taskId, triggerType, runtimeInstanceId) VALUES (?, ?, ?, 'task_assigned', ?)",
349
+ [crypto.randomUUID(), warm.id, active.id, runtime!.id],
350
+ );
351
+ const task = await createTaskExtended("queued");
352
+ expect(await routePoolTasks()).toEqual([]);
353
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
354
+ });
355
+ test.each([
356
+ "disabled",
357
+ "unbound",
358
+ "missing",
359
+ "static",
360
+ ])("reroutes an unmarked pending task when its pool becomes %s", async (reason) => {
361
+ const holder = await sleeper("holder", 3);
362
+ const runtime = await upsertRuntimeInstance({
363
+ id: crypto.randomUUID(),
364
+ agentId: holder.id,
365
+ reportedSlots: 3,
366
+ });
367
+ await getDbClient().run("UPDATE runtime_instances SET pool_id = ? WHERE id = ?", [
368
+ pool.id,
369
+ runtime!.id,
370
+ ]);
371
+ await getDbClient().run("UPDATE agents SET status = 'idle' WHERE id = ?", [holder.id]);
372
+ const task = await createTaskExtended("pool work");
373
+ await routePoolTasks({ taskIds: [task.id] });
374
+ expect((await getTaskById(task.id))?.agentId).toBe(holder.id);
375
+ expect((await getTaskById(task.id))?.poolWakeAt).toBeUndefined();
376
+ const human = await createTaskExtended("human pin", {
377
+ agentId: holder.id,
378
+ routingReason: "human_pinned",
379
+ });
380
+ await getDbClient().run("UPDATE runtime_instances SET draining = 1 WHERE id = ?", [
381
+ runtime!.id,
382
+ ]);
383
+ if (reason === "disabled") await updateRuntimePool(pool.id, { enabled: false });
384
+ if (reason === "unbound")
385
+ await getDbClient().run("UPDATE agents SET runtime_pool_id = NULL WHERE id = ?", [holder.id]);
386
+ if (reason === "missing")
387
+ await getDbClient().run("DELETE FROM runtime_pools WHERE id = ?", [pool.id]);
388
+ if (reason === "static")
389
+ await getDbClient().run("UPDATE runtime_pools SET kind = 'static' WHERE id = ?", [pool.id]);
390
+ const warm = await live();
391
+ expect(await routePoolTasks()).toEqual([{ taskId: task.id, agentId: warm.id }]);
392
+ expect(await getTaskById(human.id)).toMatchObject({ status: "pending", agentId: holder.id });
393
+ });
394
+ test("unmarked pending release CAS preserves a concurrent claim", async () => {
395
+ const holder = await sleeper();
396
+ const task = await createTaskExtended("pending", { agentId: holder.id });
397
+ await updateRuntimePool(pool.id, { enabled: false });
398
+ const client = getDbClient();
399
+ const run = client.run.bind(client);
400
+ const writes = spyOn(client, "run").mockImplementation(async (sql, params) => {
401
+ if (sql.includes("UPDATE agent_tasks SET agentId = NULL")) await startTask(task.id);
402
+ return await run(sql, params);
403
+ });
404
+ try {
405
+ await live();
406
+ await routePoolTasks();
407
+ expect(await getTaskById(task.id)).toMatchObject({
408
+ status: "in_progress",
409
+ agentId: holder.id,
410
+ });
411
+ expect(
412
+ writes.mock.calls.some(([sql]) => sql.includes("UPDATE agent_tasks SET agentId = NULL")),
413
+ ).toBe(true);
414
+ } finally {
415
+ writes.mockRestore();
416
+ }
417
+ });
418
+ test.each([
419
+ "sleeping",
420
+ "per-task",
421
+ "backoff",
422
+ ])("preserves explicit pending demand on a wakeable %s agent", async (state) => {
423
+ const holder = await sleeper();
424
+ if (state === "per-task") {
425
+ await updateRuntimePool(pool.id, { lifecycle: "per-task" });
426
+ const completed = await createTaskExtended("previous", { agentId: holder.id });
427
+ await getDbClient().run("UPDATE agent_tasks SET status = 'completed' WHERE id = ?", [
428
+ completed.id,
429
+ ]);
430
+ const runtime = await upsertRuntimeInstance({
431
+ id: crypto.randomUUID(),
432
+ agentId: holder.id,
433
+ reportedSlots: 1,
434
+ });
435
+ await getDbClient().run(
436
+ "UPDATE runtime_instances SET task_id = ?, pool_id = ? WHERE id = ?",
437
+ [completed.id, pool.id, runtime!.id],
438
+ );
439
+ }
440
+ if (state === "backoff")
441
+ await getDbClient().run(
442
+ "UPDATE agents SET runtime_provision_attempts = 1, runtime_provision_retry_at = ? WHERE id = ?",
443
+ [new Date(Date.now() + 60000).toISOString(), holder.id],
444
+ );
445
+ const task = await createTaskExtended("explicit demand", {
446
+ agentId: holder.id,
447
+ routingReason: "skill",
448
+ });
449
+ await live();
450
+ expect(await routePoolTasks()).toEqual([]);
451
+ expect(await getTaskById(task.id)).toMatchObject({
452
+ status: "pending",
453
+ agentId: holder.id,
454
+ routingReason: "skill",
455
+ });
456
+ expect((await getTaskById(task.id))?.poolWakeAt).toBeUndefined();
457
+ });
458
+ test.each(["zero-capacity", "missing-image"])("tier 2 skips a %s pool", async (state) => {
459
+ await sleeper();
460
+ // The API now rejects imageless pools; raw SQL models a row saved before that check.
461
+ if (state === "zero-capacity") await updateRuntimePool(pool.id, { maxRuntimes: 0 });
462
+ else
463
+ await getDbClient().run(
464
+ "UPDATE runtime_pools SET image = NULL, resolved_image = NULL WHERE id = ?",
465
+ [pool.id],
466
+ );
467
+ const task = await createTaskExtended("pool work");
468
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([]);
469
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
470
+ });
471
+ test.each([
472
+ true,
473
+ false,
474
+ ])("unmarked pending provisioning stays assigned with pools enabled=%s", async (enabled) => {
475
+ const holder = await sleeper();
476
+ const task = await createTaskExtended("pending", { agentId: holder.id });
477
+ await getDbClient().run(
478
+ "INSERT INTO runtime_instances (id, agent_id, pool_id, status, reported_slots) VALUES (?, ?, ?, 'provisioning', 1)",
479
+ [crypto.randomUUID(), holder.id, pool.id],
480
+ );
481
+ await live();
482
+ process.env.RUNTIME_POOLS_ENABLED = String(enabled);
483
+ expect(await routePoolTasks()).toEqual([]);
484
+ expect(await getTaskById(task.id)).toMatchObject({ status: "pending", agentId: holder.id });
485
+ });
486
+ test("flag off preserves unmarked assignments without a runtime", async () => {
487
+ const holder = await sleeper();
488
+ const task = await createTaskExtended("pending", { agentId: holder.id });
489
+ await live();
490
+ process.env.RUNTIME_POOLS_ENABLED = "false";
491
+ expect(await routePoolTasks()).toEqual([]);
492
+ expect(await getTaskById(task.id)).toMatchObject({ status: "pending", agentId: holder.id });
493
+ });
494
+ test("flag off retains the legacy runtime eligibility check", async () => {
495
+ const warm = await live("idle", 3);
496
+ await getDbClient().run(
497
+ "UPDATE runtime_instances SET draining = 1, credential_ready = 0, reported_slots = 1 WHERE agent_id = ?",
498
+ [warm.id],
499
+ );
500
+ process.env.RUNTIME_POOLS_ENABLED = "false";
501
+ const first = await createTaskExtended("first");
502
+ const second = await createTaskExtended("second");
503
+ expect(await routePoolTasks()).toEqual([
504
+ { taskId: first.id, agentId: warm.id },
505
+ { taskId: second.id, agentId: warm.id },
506
+ ]);
507
+ });
508
+ test("unmarked pending work waits for an occupied eligible runtime", async () => {
509
+ const holder = await sleeper("holder", 2);
510
+ const runtime = await upsertRuntimeInstance({
511
+ id: crypto.randomUUID(),
512
+ agentId: holder.id,
513
+ reportedSlots: 1,
514
+ });
515
+ await getDbClient().run("UPDATE agents SET status = 'busy' WHERE id = ?", [holder.id]);
516
+ const active = await createTaskExtended("active", { agentId: holder.id });
517
+ await startTask(active.id);
518
+ await getDbClient().run("UPDATE runtime_instances SET task_id = ? WHERE id = ?", [
519
+ active.id,
520
+ runtime!.id,
521
+ ]);
522
+ const pending = await createTaskExtended("waiting", { agentId: holder.id });
523
+ await live();
524
+ expect(await routePoolTasks()).toEqual([]);
525
+ expect(await getTaskById(pending.id)).toMatchObject({ status: "pending", agentId: holder.id });
526
+ });
527
+ test("tier 1 keeps healthy runtimes eligible while their pool circuit is open", async () => {
528
+ const agent = await sleeper();
529
+ await upsertRuntimeInstance({ id: crypto.randomUUID(), agentId: agent.id, reportedSlots: 1 });
530
+ await getDbClient().run("UPDATE agents SET status = 'idle' WHERE id = ?", [agent.id]);
531
+ await getDbClient().run("UPDATE runtime_pools SET circuit_open_until = ? WHERE id = ?", [
532
+ new Date(Date.now() + 60000).toISOString(),
533
+ pool.id,
534
+ ]);
535
+ const task = await createTaskExtended("pool work");
536
+ expect(await routePoolTasks()).toEqual([{ taskId: task.id, agentId: agent.id }]);
537
+ expect((await getTaskById(task.id))?.poolWakeAt).toBeUndefined();
538
+ });
539
+ test("tier 2 waits for grace and preserves the existing routing reason", async () => {
540
+ const agent = await sleeper();
541
+ const task = await createTaskExtended("pool work");
542
+ expect(await routePoolTasks({ now: new Date(task.createdAt) })).toEqual([]);
543
+ const now = afterGrace(task);
544
+ await routePoolTasks({ now });
545
+ expect(await getTaskById(task.id)).toMatchObject({
546
+ status: "pending",
547
+ agentId: agent.id,
548
+ poolWakeAt: now.toISOString(),
549
+ });
550
+ expect((await getTaskById(task.id))?.routingReason).toBe(task.routingReason);
551
+ });
552
+ test("flag off disables wake and keeps heartbeat idle-worker dispatch", async () => {
553
+ process.env.RUNTIME_POOLS_ENABLED = "false";
554
+ process.env.MULTI_RUNTIME_ENABLED = "false";
555
+ await sleeper();
556
+ const busy = await live("busy", 2);
557
+ const task = await createTaskExtended("pool work");
558
+ await createTaskExtended("active", { agentId: busy.id }).then((t) => startTask(t.id));
559
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([]);
560
+ const idle = await createAgent({ name: "legacy", isLead: false, status: "idle" });
561
+ const findings = await codeLevelTriage();
562
+ expect(findings.autoAssigned).toContainEqual({ taskId: task.id, agentId: idle.id });
563
+ });
564
+ test("legacy empty-poll and runtime gates remain effective", async () => {
565
+ process.env.RUNTIME_POOLS_ENABLED = "false";
566
+ const warm = await live();
567
+ const task = await createTaskExtended("pool work");
568
+ await getDbClient().run("UPDATE agents SET emptyPollCount = ? WHERE id = ?", [
569
+ MAX_EMPTY_POLLS,
570
+ warm.id,
571
+ ]);
572
+ expect(await routePoolTasks()).toEqual([]);
573
+ await getDbClient().run("UPDATE agents SET emptyPollCount = 0 WHERE id = ?", [warm.id]);
574
+ await getDbClient().run("UPDATE runtime_instances SET status = 'offline'");
575
+ expect(await routePoolTasks()).toEqual([]);
576
+ process.env.MULTI_RUNTIME_ENABLED = "false";
577
+ expect(await routePoolTasks()).toEqual([{ taskId: task.id, agentId: warm.id }]);
578
+ });
579
+ test("busy live agent with spare capacity wins over wake", async () => {
580
+ await sleeper();
581
+ const warm = await live("busy", 2);
582
+ const active = await createTaskExtended("active", { agentId: warm.id });
583
+ await startTask(active.id);
584
+ const task = await createTaskExtended("pool work");
585
+ await routePoolTasks({ now: afterGrace(task) });
586
+ expect(await getTaskById(task.id)).toMatchObject({ agentId: warm.id, status: "pending" });
587
+ expect((await getTaskById(task.id))?.poolWakeAt).toBeUndefined();
588
+ });
589
+ test("enabled routing uses live capacity despite a legacy empty-poll count", async () => {
590
+ const warm = await live();
591
+ await getDbClient().run("UPDATE agents SET emptyPollCount = ? WHERE id = ?", [
592
+ MAX_EMPTY_POLLS,
593
+ warm.id,
594
+ ]);
595
+ const task = await createTaskExtended("pool work");
596
+ expect(await routePoolTasks()).toEqual([{ taskId: task.id, agentId: warm.id }]);
597
+ });
598
+ test("concurrent routing does not exceed agent headroom", async () => {
599
+ const agent = await sleeper();
600
+ const first = await createTaskExtended("first");
601
+ const second = await createTaskExtended("second");
602
+ const now = afterGrace(second);
603
+ const assignments = await Promise.all([
604
+ routePoolTasks({ taskIds: [first.id], now }),
605
+ routePoolTasks({ taskIds: [second.id], now }),
606
+ ]);
607
+ expect(assignments.flat()).toHaveLength(1);
608
+ expect(assignments.flat()[0]?.agentId).toBe(agent.id);
609
+ const count = await getDbClient().get<{ n: number }>(
610
+ "SELECT COUNT(*) AS n FROM agent_tasks WHERE agentId = ? AND status = 'pending'",
611
+ [agent.id],
612
+ );
613
+ expect(count?.n).toBe(1);
614
+ });
615
+ test("tier 2 applies role and capability affinity", async () => {
616
+ await sleeper("wrong", 3);
617
+ const match = await sleeper("match");
618
+ await updateAgentProfile(match.id, { role: "coder", capabilities: ["git"] });
619
+ const task = await createTaskExtended("pool work", {
620
+ routingAffinity: { role: "coder", capabilities: ["git"], leadOnly: false },
621
+ });
622
+ await routePoolTasks({ now: afterGrace(task) });
623
+ expect((await getTaskById(task.id))?.agentId).toBe(match.id);
624
+ const missing = await createTaskExtended("missing capability", {
625
+ routingAffinity: { capabilities: ["deploy"], leadOnly: false },
626
+ });
627
+ expect(await routePoolTasks({ now: afterGrace(missing) })).toEqual([]);
628
+ });
629
+ test("tier 2 applies pinned-model harness compatibility", async () => {
630
+ await createPoolAgent(pool.id, { name: "codex", harnessProvider: "codex", maxTasks: 3 });
631
+ const claude = await createPoolAgent(pool.id, { name: "claude", harnessProvider: "claude" });
632
+ const task = await createTaskExtended("pool work", { model: "claude-sonnet-4-5" });
633
+ await routePoolTasks({ now: afterGrace(task) });
634
+ expect((await getTaskById(task.id))?.agentId).toBe(claude.id);
635
+ });
636
+ test.each(["disabled", "circuit", "static"])("tier 2 skips %s pools", async (reason) => {
637
+ await sleeper();
638
+ if (reason === "disabled") await updateRuntimePool(pool.id, { enabled: false });
639
+ if (reason === "circuit")
640
+ await getDbClient().run("UPDATE runtime_pools SET circuit_open_until = ?", [
641
+ new Date(Date.now() + 120000).toISOString(),
642
+ ]);
643
+ if (reason === "static") await getDbClient().run("UPDATE runtime_pools SET kind = 'static'");
644
+ const task = await createTaskExtended("pool work");
645
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([]);
646
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
647
+ });
648
+ test.each([
649
+ "global",
650
+ "agent",
651
+ "user",
652
+ ] as const)("%s budget refusal leaves work unassigned", async (scope) => {
653
+ const agent = await sleeper();
654
+ const user = await createUser({
655
+ email: `budget-routing-${crypto.randomUUID()}@example.com`,
656
+ name: "requester",
657
+ });
658
+ await upsertBudget(scope, scope === "global" ? "" : scope === "agent" ? agent.id : user.id, 0);
659
+ const task = await createTaskExtended("pool work", { requestedByUserId: user.id });
660
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([]);
661
+ expect(await getTaskById(task.id)).toMatchObject({ status: "unassigned" });
662
+ });
663
+ test("agent A refused, agent B allowed", async () => {
664
+ const a = await sleeper("A");
665
+ const b = await sleeper("B");
666
+ await upsertBudget("agent", a.id, 0);
667
+ const task = await createTaskExtended("pool work");
668
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([
669
+ { taskId: task.id, agentId: b.id },
670
+ ]);
671
+ });
672
+ test("timeout chooses another sleeping agent without a warm agent", async () => {
673
+ const first = await sleeper("A");
674
+ const second = await sleeper("B");
675
+ const task = await createTaskExtended("pool work");
676
+ const at = afterGrace(task);
677
+ await routePoolTasks({ now: at });
678
+ expect((await getTaskById(task.id))?.agentId).toBe(first.id);
679
+ await routePoolTasks({ now: new Date(at.getTime() + 600001) });
680
+ expect((await getTaskById(task.id))?.agentId).toBe(second.id);
681
+ });
682
+ test("timeout cannot retry the old agent when the alternative refuses budget", async () => {
683
+ await sleeper("A");
684
+ const other = await sleeper("B");
685
+ const task = await createTaskExtended("pool work");
686
+ const at = afterGrace(task);
687
+ await routePoolTasks({ now: at });
688
+ await upsertBudget("agent", other.id, 0);
689
+ expect(await routePoolTasks({ now: new Date(at.getTime() + 600001) })).toEqual([]);
690
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
691
+ });
692
+ test.each([
693
+ false,
694
+ true,
695
+ ])("sole sleeping candidate retries only with a closed circuit (open=%s)", async (open) => {
696
+ const sleeping = await sleeper();
697
+ const task = await createTaskExtended("pool work");
698
+ const at = afterGrace(task);
699
+ await routePoolTasks({ now: at });
700
+ const now = new Date(at.getTime() + 600001);
701
+ if (open)
702
+ await getDbClient().run("UPDATE runtime_pools SET circuit_open_until = ?", [
703
+ new Date(now.getTime() + 60000).toISOString(),
704
+ ]);
705
+ await routePoolTasks({ now });
706
+ expect(await getTaskById(task.id)).toMatchObject(
707
+ open
708
+ ? { status: "unassigned" }
709
+ : { status: "pending", agentId: sleeping.id, poolWakeAt: now.toISOString() },
710
+ );
711
+ });
712
+ test("selects most headroom and breaks ties by name, reserving pending slots", async () => {
713
+ const beta = await sleeper("beta", 3);
714
+ const alpha = await sleeper("alpha", 3);
715
+ await sleeper("small", 1);
716
+ await createTaskExtended("reserved", { agentId: beta.id, routingReason: "human_pinned" });
717
+ const first = await createTaskExtended("first");
718
+ const second = await createTaskExtended("second");
719
+ const third = await createTaskExtended("third");
720
+ await routePoolTasks({ now: afterGrace(third) });
721
+ expect((await getTaskById(first.id))?.agentId).toBe(alpha.id);
722
+ expect((await getTaskById(second.id))?.agentId).toBe(alpha.id);
723
+ expect((await getTaskById(third.id))?.agentId).toBe(beta.id);
724
+ });
725
+ test("leads and agents without headroom never take pool tasks", async () => {
726
+ await createPoolAgent(pool.id, { name: "lead", isLead: true });
727
+ const agent = await sleeper();
728
+ await createTaskExtended("reserved", { agentId: agent.id, routingReason: "human_pinned" });
729
+ const task = await createTaskExtended("pool work");
730
+ expect(await routePoolTasks({ now: afterGrace(task) })).toEqual([]);
731
+ });
732
+ test.each([
733
+ "timeout",
734
+ "circuit",
735
+ ])("reroutes marked wakes on %s but preserves human pins", async (reason) => {
736
+ const sleeping = await sleeper();
737
+ const task = await createTaskExtended("pool work");
738
+ const at = afterGrace(task);
739
+ await routePoolTasks({ now: at });
740
+ const human = await createTaskExtended("human pin", {
741
+ agentId: sleeping.id,
742
+ routingReason: "human_pinned",
743
+ });
744
+ const warm = await live();
745
+ if (reason === "circuit")
746
+ await getDbClient().run("UPDATE runtime_pools SET circuit_open_until = ?", [
747
+ new Date(at.getTime() + 1200000).toISOString(),
748
+ ]);
749
+ const now = reason === "timeout" ? new Date(at.getTime() + 600001) : at;
750
+ await routePoolTasks({ now });
751
+ expect(await getTaskById(task.id)).toMatchObject({ status: "pending", agentId: warm.id });
752
+ expect((await getTaskById(task.id))?.poolWakeAt).toBeUndefined();
753
+ expect(await getTaskById(human.id)).toMatchObject({ status: "pending", agentId: sleeping.id });
754
+ });
755
+ test("pending wake stays pinned before timeout", async () => {
756
+ const sleeping = await sleeper();
757
+ const task = await createTaskExtended("pool work");
758
+ const at = afterGrace(task);
759
+ await routePoolTasks({ now: at });
760
+ await live();
761
+ await routePoolTasks({ now: new Date(at.getTime() + 599000) });
762
+ expect((await getTaskById(task.id))?.agentId).toBe(sleeping.id);
763
+ });
764
+ test("creation routes immediately and ignores offered tasks", async () => {
765
+ const warm = await live("idle", 2);
766
+ startPoolRouting();
767
+ const task = await createTaskExtended("pool work");
768
+ await waitForAssignment(task.id, warm.id);
769
+ const offer = await createTaskExtended("offered work", { offeredTo: warm.id });
770
+ expect(await routePoolTasks({ taskIds: [offer.id] })).toEqual([]);
771
+ expect((await getTaskById(offer.id))?.status).toBe("offered");
772
+ });
773
+ test.each([
774
+ "release",
775
+ "reject",
776
+ ])("%s routes after commit and suppresses rollback events", async (action) => {
777
+ const holder = await sleeper();
778
+ const warm = await live();
779
+ const task = await createTaskExtended(
780
+ "held",
781
+ action === "release" ? { agentId: holder.id } : { offeredTo: holder.id },
782
+ );
783
+ startPoolRouting();
784
+ const mutation = () =>
785
+ action === "release" ? releaseTask(task.id) : rejectTask(task.id, holder.id);
786
+ await expect(
787
+ getDbClient().transaction(async () => {
788
+ await mutation();
789
+
790
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
791
+ throw new Error("rollback");
792
+ }),
793
+ ).rejects.toThrow("rollback");
794
+ expect(await routePoolTasks({ taskIds: [task.id] })).toEqual([]);
795
+ expect((await getTaskById(task.id))?.status).toBe(action === "release" ? "pending" : "offered");
796
+ await mutation();
797
+ await waitForAssignment(task.id, warm.id);
798
+ });
799
+ test("timer routes after grace and stop removes routing triggers", async () => {
800
+ process.env.RUNTIME_WAKE_GRACE_SEC = "0";
801
+ process.env.RUNTIME_ROUTING_INTERVAL_MS = "20";
802
+ const sleeping = await sleeper("sleeping", 2);
803
+ const task = await createAfterEventDelivery();
804
+ startPoolRouting();
805
+ await waitForAssignment(task.id, sleeping.id);
806
+ const off = spyOn(workflowEventBus, "off");
807
+ try {
808
+ stopPoolRouting();
809
+ expect(off.mock.calls.map(([event]) => event)).toEqual(["task.created", "task.unassigned"]);
810
+ } finally {
811
+ off.mockRestore();
812
+ }
813
+ const later = await createTaskExtended("after stop");
814
+
815
+ expect((await getTaskById(later.id))?.status).toBe("unassigned");
816
+ });
817
+ });