@desplega.ai/agent-swarm 1.169.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 (349) hide show
  1. package/README.md +11 -2
  2. package/dist/{acp-adapter-m47zfx4a.js → acp-adapter-1z9p3fvx.js} +6 -6
  3. package/dist/{actions-ws7k3tvq.js → actions-rc2yjk8a.js} +31 -29
  4. package/dist/{amp-adapter-nvv8k8j5.js → amp-adapter-ypyf5s1x.js} +6 -6
  5. package/dist/{anthropic-messages-13yn502s.js → anthropic-messages-etkbxhhy.js} +11 -11
  6. package/dist/{app-80w6qspx.js → app-04ab2s0z.js} +9 -9
  7. package/dist/{artifact-fey7dvj9.js → artifact-eczsknc3.js} +6 -3
  8. package/dist/{assistant-jtq439kw.js → assistant-vhmjd8wf.js} +34 -32
  9. package/dist/{azure-openai-responses-9st34pfz.js → azure-openai-responses-xap57x4b.js} +7 -7
  10. package/dist/{boot-reembed-feda8wn8.js → boot-reembed-3p0m259x.js} +8 -8
  11. package/dist/{boot-reembed-3nc0aaed.js → boot-reembed-wew3gwk8.js} +8 -8
  12. package/dist/{boot-scrub-sweep-grsa2er0.js → boot-scrub-sweep-5ytwtq24.js} +9 -9
  13. package/dist/{boot-seeds-gcfgq44f.js → boot-seeds-s8p2xry8.js} +12 -12
  14. package/dist/{claude-adapter-pjv2q7t8.js → claude-adapter-akrzg6jh.js} +15 -15
  15. package/dist/{claude-managed-adapter-0wa8se82.js → claude-managed-adapter-kqqqhp2q.js} +2 -2
  16. package/dist/{claude-managed-setup-zxa9p9tx.js → claude-managed-setup-bnmjarjc.js} +11 -8
  17. package/dist/{claude-sdk-session-c9j6k9kx.js → claude-sdk-session-exq1jadx.js} +15 -15
  18. package/dist/{cli-a8vps458.js → cli-01qndrvw.js} +17274 -17242
  19. package/dist/{cli-rg1c8ccw.js → cli-0d2rqhvq.js} +497 -73
  20. package/dist/cli-0p3enk35.js +188 -0
  21. package/dist/cli-1srmzpr3.js +16 -0
  22. package/dist/{cli-zzev32jw.js → cli-231av4y6.js} +6 -6
  23. package/dist/{cli-kp3vfknv.js → cli-280gnz4v.js} +2 -2
  24. package/dist/{cli-mv2520nm.js → cli-2bw3w47j.js} +1 -1
  25. package/dist/{cli-km8559dm.js → cli-2qbs2tvf.js} +18 -10
  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-jkfxpg1b.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-qg2zhjfc.js → cli-62bwwc2q.js} +8 -8
  32. package/dist/{cli-s0k2j7q9.js → cli-6xxxb8qt.js} +10 -10
  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-rbr91whd.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-dv76vq5d.js → cli-ctret352.js} +685 -209
  42. package/dist/{cli-wt7ccy4r.js → cli-d161g69q.js} +9 -1
  43. package/dist/cli-db3m6zdt.js +362 -0
  44. package/dist/{cli-hf5s9pgf.js → cli-e3pmyt39.js} +1 -1
  45. package/dist/{cli-ysnkvdx6.js → cli-ec7hrgqp.js} +313 -91
  46. package/dist/{cli-m23r88re.js → cli-erbp6b4g.js} +3 -3
  47. package/dist/{cli-9k7mqz00.js → cli-ge593j7e.js} +30 -3
  48. package/dist/cli-h8w3fg83.js +17 -0
  49. package/dist/{cli-xvdgasgy.js → cli-hsem16hb.js} +114 -9
  50. package/dist/{cli-yvxfaem2.js → cli-hsk5gd15.js} +9 -5
  51. package/dist/{cli-7vz49g7h.js → cli-jxsgrnmq.js} +101 -4
  52. package/dist/{cli-b7y8113e.js → cli-k2v5x13t.js} +1 -1
  53. package/dist/{cli-bqrs5ycj.js → cli-m4csbs67.js} +2 -2
  54. package/dist/{cli-91r17hb1.js → cli-mzj3aqac.js} +3 -3
  55. package/dist/{cli-swrz3mj8.js → cli-n805bb98.js} +1 -1
  56. package/dist/{cli-sajr098b.js → cli-n8ybnv1k.js} +3 -3
  57. package/dist/{cli-e5qk3m2a.js → cli-nkjwe4z7.js} +19 -19
  58. package/dist/{cli-3ry5gx4f.js → cli-p2pjfq1r.js} +2 -2
  59. package/dist/{cli-61tbnjpf.js → cli-p8f7dqgt.js} +767 -541
  60. package/dist/{cli-r7eeddkq.js → cli-ph3145b7.js} +3 -3
  61. package/dist/cli-ppy4nnnh.js +168 -0
  62. package/dist/cli-r455yd58.js +832 -0
  63. package/dist/{cli-jv36y8wz.js → cli-rrbctenq.js} +1 -1
  64. package/dist/{cli-8xjst8q2.js → cli-s88my3br.js} +11 -11
  65. package/dist/{cli-vejds37t.js → cli-srmej5tp.js} +3 -3
  66. package/dist/{cli-eqfbsxsn.js → cli-tg8q0b5y.js} +14 -11
  67. package/dist/cli-w0zx9ya1.js +1351 -0
  68. package/dist/{cli-rh44xx46.js → cli-wfvedq5w.js} +15 -15
  69. package/dist/{cli-n4z21pt8.js → cli-x207g4w1.js} +1 -1
  70. package/dist/{cli-fyeft04q.js → cli-x3fy2tgp.js} +2 -2
  71. package/dist/{cli-7mhy3ths.js → cli-x61srfpp.js} +5 -5
  72. package/dist/{cli-w6xa5nf5.js → cli-y1j1ggbf.js} +8 -8
  73. package/dist/{cli-jvmtxqtc.js → cli-ywh2a87k.js} +7 -2
  74. package/dist/{cli-3qhyh31j.js → cli-ywm1azg9.js} +6 -0
  75. package/dist/{cli-z48ggfyq.js → cli-zxm9p3x4.js} +2 -2
  76. package/dist/cli.js +42 -24
  77. package/dist/{codex-adapter-0kpg09ca.js → codex-adapter-9ag75whm.js} +13 -13
  78. package/dist/{codex-hook-295jpbfa.js → codex-hook-7ba15hjd.js} +5 -3
  79. package/dist/{codex-login-g1cyt5zm.js → codex-login-841kv5fk.js} +7 -5
  80. package/dist/{codex-session-runner-8n672hxh.js → codex-session-runner-9fp7y4cf.js} +13 -13
  81. package/dist/{commands-mhmg5n0x.js → commands-d7eah0y9.js} +10 -10
  82. package/dist/controller-hq5ggpcp.js +193 -0
  83. package/dist/controllers-connect-8t0k65x2.js +265 -0
  84. package/dist/{cursor-adapter-wn8n5zzk.js → cursor-adapter-t4pr1m3c.js} +5 -5
  85. package/dist/db-2rmd23wq.js +71 -0
  86. package/dist/{db-wa6s1mwv.js → db-sp39qv72.js} +8 -8
  87. package/dist/{devin-adapter-bffjj5n4.js → devin-adapter-xgtj680j.js} +2 -2
  88. package/dist/{dispatcher-nhs2pkr8.js → dispatcher-g7fsybsd.js} +30 -28
  89. package/dist/docker-f503k9p0.js +201 -0
  90. package/dist/{dsh-adapter-5423p70m.js → dsh-adapter-2h8m4cab.js} +5 -5
  91. package/dist/e2b-66xra4az.js +202 -0
  92. package/dist/{e2b-nd50eccg.js → e2b-xwhyj8cw.js} +43 -527
  93. package/dist/{event-bus-vnfthafg.js → event-bus-vjtv82gm.js} +2 -2
  94. package/dist/{grok-adapter-28pkdt80.js → grok-adapter-1err1me7.js} +7 -7
  95. package/dist/{handlers-5rtf9nng.js → handlers-c7yggm37.js} +33 -31
  96. package/dist/heartbeat-dkm924fk.js +124 -0
  97. package/dist/{hook-qkm73zk6.js → hook-g8e7sfnf.js} +13 -13
  98. package/dist/{hook-krjfkpzy.js → hook-wgx4exw6.js} +20 -17
  99. package/dist/{http-a397xcn4.js → http-1d23k928.js} +1958 -205
  100. package/dist/{index-kqre45fr.js → index-gm10tmye.js} +30 -28
  101. package/dist/{index-rtxg15ah.js → index-q7cj9bn2.js} +39 -29
  102. package/dist/{index-vvsq7v45.js → index-va4zbvzp.js} +30 -28
  103. package/dist/{index-ac0v8v9d.js → index-vertaxs1.js} +8 -8
  104. package/dist/{index-ytdhtdry.js → index-y5vvczrs.js} +31 -22
  105. package/dist/{keepalive-psfh8ysx.js → keepalive-1jc5e7ct.js} +10 -10
  106. package/dist/{lead-m6rppjn7.js → lead-yxw2f1sg.js} +48 -46
  107. package/dist/{maintenance-41f837ke.js → maintenance-h7w2m10y.js} +8 -8
  108. package/dist/{node-8zkdq42h.js → node-c9we95xf.js} +9 -9
  109. package/dist/{oauth-refresh-sweep-6hr55hm8.js → oauth-refresh-sweep-wh9ypd1b.js} +9 -9
  110. package/dist/{onboard-ktrtd7cz.js → onboard-dfr9j762.js} +25 -15
  111. package/dist/{openai-codex-responses-jzxkta9b.js → openai-codex-responses-pgvtddvv.js} +3 -3
  112. package/dist/{openai-responses-m3g43rrt.js → openai-responses-9dyzwzkr.js} +9 -9
  113. package/dist/{opencode-adapter-yjyqynzf.js → opencode-adapter-f1zby94p.js} +3 -3
  114. package/dist/{otel-impl-dq4f2k3m.js → otel-impl-and31m5s.js} +5 -5
  115. package/dist/{pi-mono-adapter-e5v9w0hv.js → pi-mono-adapter-fx6k5wwh.js} +38 -27
  116. package/dist/{pricing-refresh-kmk7j1ra.js → pricing-refresh-23zr6j3w.js} +10 -10
  117. package/dist/{rbac-roles-9ykx9tc5.js → rbac-roles-nc7k9d0t.js} +10 -10
  118. package/dist/{rbac-roles-9w1dkkxd.js → rbac-roles-wnx35x4x.js} +11 -11
  119. package/dist/{render-v2-ycqm53np.js → render-v2-81cg1y52.js} +9 -9
  120. package/dist/{resume-cq2pkp8q.js → resume-d4b6j2n2.js} +16 -16
  121. package/dist/{setup-0dcmdvbn.js → setup-g68r1w28.js} +7 -5
  122. package/dist/static-t1gr2cr7.js +30 -0
  123. package/dist/{virtual-modules-1x9k4jcs.js → virtual-modules-rdjmcg7q.js} +3 -3
  124. package/dist/{work-objects-x01x2fh8.js → work-objects-0zzdnbt9.js} +35 -33
  125. package/dist/{worker-py1ssb1h.js → worker-p1g8qm6h.js} +48 -46
  126. package/dist/{x-ma4gnfgy.js → x-qfmygqqv.js} +2 -2
  127. package/openapi.json +3382 -167
  128. package/package.json +3 -3
  129. package/src/be/db/agents.ts +6 -0
  130. package/src/be/db/instance-signals.ts +30 -0
  131. package/src/be/db/tasks/read.ts +2 -0
  132. package/src/be/db/tasks/write.ts +39 -5
  133. package/src/be/db/workflow-runs.ts +22 -0
  134. package/src/be/db.ts +96 -40
  135. package/src/be/fleet-benchmark-stats.ts +39 -0
  136. package/src/be/migrations/205_favorites_per_dashboard_user.sql +29 -0
  137. package/src/be/migrations/206_runtime_pools.sql +62 -0
  138. package/src/be/migrations/207_runtime_session_tokens.sql +31 -0
  139. package/src/be/migrations/208_task_runtime_dispatch.sql +4 -0
  140. package/src/be/model-tier-resolution.ts +9 -2
  141. package/src/be/multi-runtime.ts +102 -13
  142. package/src/be/org-profile.ts +35 -0
  143. package/src/be/rbac-audit.ts +4 -0
  144. package/src/be/runtime-pools/controllers.ts +150 -0
  145. package/src/be/runtime-pools/db.ts +526 -0
  146. package/src/be/runtime-pools/demand.ts +65 -0
  147. package/src/be/runtime-pools/e2b-images.ts +109 -0
  148. package/src/be/runtime-pools/errors.ts +8 -0
  149. package/src/be/runtime-pools/lifecycle.ts +44 -0
  150. package/src/be/runtime-pools/reachability.ts +30 -0
  151. package/src/be/runtime-pools/recovery.ts +248 -0
  152. package/src/be/runtime-pools/remote-controller.ts +144 -0
  153. package/src/be/runtime-pools/routing.ts +304 -0
  154. package/src/be/runtime-pools/seed.ts +110 -0
  155. package/src/be/runtime-pools/supervisor.ts +981 -0
  156. package/src/be/runtime-pools/tokens.ts +57 -0
  157. package/src/be/scripts/typecheck.ts +48 -3
  158. package/src/be/secret-registry.ts +17 -1
  159. package/src/be/skill-usage.ts +467 -0
  160. package/src/be/swarm-config-guard.ts +59 -2
  161. package/src/be/telemetry-enable.ts +66 -0
  162. package/src/be/telemetry-ingest-anchor.ts +77 -0
  163. package/src/be/users.ts +173 -23
  164. package/src/be/worker-nudge-listener.ts +28 -0
  165. package/src/be/worker-nudges.ts +22 -0
  166. package/src/cli.tsx +22 -3
  167. package/src/commands/controller.ts +180 -0
  168. package/src/commands/onboard/non-interactive.ts +36 -12
  169. package/src/commands/onboard/types.ts +1 -1
  170. package/src/commands/onboard.tsx +3 -0
  171. package/src/commands/push-delivery.ts +230 -0
  172. package/src/commands/runner.ts +560 -276
  173. package/src/e2b/dispatch.ts +15 -1
  174. package/src/extensions/contract-types.generated.ts +1 -1
  175. package/src/fleet-benchmark.ts +157 -0
  176. package/src/heartbeat/heartbeat.ts +53 -135
  177. package/src/hooks/hook.ts +3 -2
  178. package/src/http/agents.ts +21 -0
  179. package/src/http/all-routes.ts +6 -0
  180. package/src/http/apps.ts +4 -2
  181. package/src/http/auth.ts +4 -7
  182. package/src/http/config.ts +58 -21
  183. package/src/http/controllers-connect.ts +206 -0
  184. package/src/http/core.ts +108 -14
  185. package/src/http/favorite-owner.ts +31 -1
  186. package/src/http/fleet-benchmark.ts +206 -0
  187. package/src/http/index.ts +38 -6
  188. package/src/http/mcp.ts +3 -8
  189. package/src/http/poll.ts +31 -2
  190. package/src/http/realtime.ts +1 -1
  191. package/src/http/request-principal.ts +2 -0
  192. package/src/http/runtime-pools.ts +527 -0
  193. package/src/http/session-data.ts +2 -0
  194. package/src/http/sessions.ts +100 -23
  195. package/src/http/skills.ts +173 -0
  196. package/src/http/status.ts +25 -1
  197. package/src/http/tasks.ts +14 -1
  198. package/src/http/telemetry-ui.ts +102 -0
  199. package/src/http/used-by.ts +140 -0
  200. package/src/http/utils.ts +7 -1
  201. package/src/http/workflows.ts +10 -2
  202. package/src/jira/app.ts +1 -2
  203. package/src/kv-reserved-namespaces.ts +1 -0
  204. package/src/linear/app.ts +1 -2
  205. package/src/providers/acp-target-catalog.ts +75 -1
  206. package/src/providers/acp-targets.ts +114 -0
  207. package/src/providers/amp-adapter.ts +1 -1
  208. package/src/providers/claude-adapter.ts +1 -1
  209. package/src/providers/codex-adapter.ts +5 -1
  210. package/src/providers/opencode-adapter.ts +2 -1
  211. package/src/providers/pi-mono-adapter.ts +41 -26
  212. package/src/providers/pricing-sources.md +2 -0
  213. package/src/providers/skill-invoke.ts +39 -3
  214. package/src/rbac/legacy-policy.ts +8 -0
  215. package/src/rbac/permissions.ts +16 -0
  216. package/src/rbac/types.ts +2 -0
  217. package/src/realtime/auth.ts +13 -0
  218. package/src/realtime/channels.ts +24 -0
  219. package/src/realtime/transport.ts +94 -15
  220. package/src/realtime/worker-client.ts +176 -0
  221. package/src/runtime-providers/controller-protocol.ts +97 -0
  222. package/src/runtime-providers/controller.ts +86 -0
  223. package/src/runtime-providers/docker.ts +235 -0
  224. package/src/runtime-providers/e2b.ts +217 -0
  225. package/src/runtime-providers/index.ts +37 -0
  226. package/src/runtime-providers/static.ts +26 -0
  227. package/src/runtime-providers/types.ts +92 -0
  228. package/src/runtime-providers/worker-api-url.ts +11 -0
  229. package/src/scripts-runtime/sdk-allowlist.ts +6 -0
  230. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  231. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  232. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  233. package/src/server.ts +12 -0
  234. package/src/slack/router.ts +7 -5
  235. package/src/tasks/worker-follow-up.ts +80 -52
  236. package/src/telemetry-context.ts +6 -0
  237. package/src/telemetry-contract/EVENTS.md +14 -1
  238. package/src/telemetry-contract/SOURCE.json +4 -4
  239. package/src/telemetry-contract/catalog.json +155 -0
  240. package/src/telemetry-contract/types.gen.ts +248 -4
  241. package/src/telemetry-ui-events.ts +130 -0
  242. package/src/telemetry.ts +43 -3
  243. package/src/tests/acp-adapter.test.ts +129 -0
  244. package/src/tests/claude-adapter.test.ts +6 -2
  245. package/src/tests/codex-adapter.test.ts +6 -2
  246. package/src/tests/config-internal-keys.test.ts +27 -0
  247. package/src/tests/config-session-auth.test.ts +19 -3
  248. package/src/tests/db-retention.test.ts +50 -10
  249. package/src/tests/docs-index-footprint.test.js +3 -3
  250. package/src/tests/e2b-dispatch.test.ts +28 -1
  251. package/src/tests/e2e-runtime-pools.test.ts +51 -0
  252. package/src/tests/entrypoint-acp-target-binary.test.ts +38 -1
  253. package/src/tests/favorites.test.ts +188 -0
  254. package/src/tests/fetch-resolved-env.test.ts +59 -1
  255. package/src/tests/fleet-benchmark.test.ts +453 -0
  256. package/src/tests/harness-model-support.test.ts +70 -2
  257. package/src/tests/heartbeat-orphan-resume.test.ts +24 -0
  258. package/src/tests/heartbeat-reboot-live.test.ts +31 -0
  259. package/src/tests/heartbeat-supersede-resume.test.ts +21 -0
  260. package/src/tests/hook-runtime-close.test.ts +64 -0
  261. package/src/tests/http-task-write-ownership.test.ts +8 -1
  262. package/src/tests/integration-disable-flags.test.ts +45 -0
  263. package/src/tests/migration-206-runtime-pools.test.ts +134 -0
  264. package/src/tests/migration-207-runtime-session-tokens.test.ts +45 -0
  265. package/src/tests/migration-208-task-runtime-dispatch.test.ts +30 -0
  266. package/src/tests/multi-runtime-registration.test.ts +219 -4
  267. package/src/tests/onboard-non-interactive.test.ts +104 -6
  268. package/src/tests/opencode-adapter.test.ts +6 -2
  269. package/src/tests/org-website.test.ts +115 -0
  270. package/src/tests/pi-mono-adapter.test.ts +74 -10
  271. package/src/tests/rbac-admission-e2e.test.ts +4 -4
  272. package/src/tests/rbac-engine.test.ts +8 -1
  273. package/src/tests/realtime-room-auth.test.ts +49 -0
  274. package/src/tests/repo-allow-merge-gate.test.ts +8 -1
  275. package/src/tests/runner-exit-after-tasks.test.ts +163 -0
  276. package/src/tests/runner-finish-retry.test.ts +466 -0
  277. package/src/tests/runner-harness-telemetry-env.test.ts +105 -0
  278. package/src/tests/runner-push-delivery.test.ts +670 -0
  279. package/src/tests/runner-slot-reconcile.test.ts +89 -1
  280. package/src/tests/runtime-child-token-cleanup.test.ts +172 -0
  281. package/src/tests/runtime-controller-token.test.ts +254 -0
  282. package/src/tests/runtime-demand.test.ts +119 -0
  283. package/src/tests/runtime-e2b-pools.test.ts +847 -0
  284. package/src/tests/runtime-pool-reachability.test.ts +328 -0
  285. package/src/tests/runtime-pool-routing.test.ts +817 -0
  286. package/src/tests/runtime-pools-db.test.ts +275 -0
  287. package/src/tests/runtime-pools-http.test.ts +506 -0
  288. package/src/tests/runtime-pools-ownership-guards.test.ts +235 -0
  289. package/src/tests/runtime-pools-seed.test.ts +203 -0
  290. package/src/tests/runtime-pools-test-utils.ts +128 -0
  291. package/src/tests/runtime-provider-registration.test.ts +133 -0
  292. package/src/tests/runtime-providers-docker.test.ts +400 -0
  293. package/src/tests/runtime-providers-e2b-transport.test.ts +50 -0
  294. package/src/tests/runtime-providers-e2b.test.ts +335 -0
  295. package/src/tests/runtime-recovery.test.ts +745 -0
  296. package/src/tests/runtime-remote-controller.test.ts +846 -0
  297. package/src/tests/runtime-supervisor-failures.test.ts +387 -0
  298. package/src/tests/runtime-supervisor.test.ts +1039 -0
  299. package/src/tests/runtime-token-auth.test.ts +897 -0
  300. package/src/tests/script-connections.test.ts +7 -0
  301. package/src/tests/secret-scrubber.test.ts +17 -2
  302. package/src/tests/session-costs-codex-recompute.test.ts +49 -0
  303. package/src/tests/session-tokens.test.ts +13 -3
  304. package/src/tests/skill-invoke.test.ts +142 -0
  305. package/src/tests/skill-usage-cohorts.test.ts +232 -0
  306. package/src/tests/skill-usage.test.ts +348 -0
  307. package/src/tests/slack-attachments-block.test.ts +23 -0
  308. package/src/tests/slack-render-v2.test.ts +6 -0
  309. package/src/tests/status-instance.test.ts +71 -0
  310. package/src/tests/task-supersede-resume.test.ts +87 -4
  311. package/src/tests/telemetry-enable.test.ts +113 -0
  312. package/src/tests/telemetry-ui-route.test.ts +195 -0
  313. package/src/tests/tool-annotations.test.ts +1 -1
  314. package/src/tests/used-by-route.test.ts +237 -0
  315. package/src/tests/worker-nudges.test.ts +653 -0
  316. package/src/tests/worker-realtime-client.test.ts +188 -0
  317. package/src/tests/workflow-sub-workflow.test.ts +22 -0
  318. package/src/tools/app-upsert.ts +1 -1
  319. package/src/tools/runtime-controller-list.ts +28 -0
  320. package/src/tools/runtime-pool-bind-agent.ts +29 -0
  321. package/src/tools/runtime-pool-common.ts +37 -0
  322. package/src/tools/runtime-pool-create-agent.ts +29 -0
  323. package/src/tools/runtime-pool-delete.ts +27 -0
  324. package/src/tools/runtime-pool-list.ts +27 -0
  325. package/src/tools/runtime-pool-upsert.ts +31 -0
  326. package/src/tools/script-common.ts +1 -1
  327. package/src/tools/send-task.ts +3 -2
  328. package/src/tools/tool-config.ts +6 -0
  329. package/src/types.ts +137 -2
  330. package/src/utils/config-secrets.ts +1 -0
  331. package/src/utils/lead-monitor-channels.ts +3 -0
  332. package/src/utils/org-website.ts +104 -0
  333. package/src/utils/request-auth-context.ts +10 -2
  334. package/src/utils/runtime-pools-flag.ts +11 -0
  335. package/src/utils/secret-scrubber.ts +9 -0
  336. package/src/utils/session-token-connections.ts +20 -0
  337. package/templates/skills/apps/content.md +1 -1
  338. package/templates/skills/code-quality/content.md +9 -1
  339. package/templates/skills/db-query-guidance/content.md +9 -0
  340. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  341. package/templates/skills/swarm-scripts/content.md +1 -1
  342. package/templates/skills/work-on-task/SKILL.md +1 -1
  343. package/templates/skills/work-on-task/content.md +1 -1
  344. package/dist/{cli-qd97v6z2.js → cli-1qtgd1h1.js} +3 -3
  345. package/dist/{cli-cmxkbzzs.js → cli-j0w59p15.js} +6 -6
  346. package/dist/{google-generative-ai-b8zxrpax.js → google-generative-ai-pvhm0dq1.js} +1 -1
  347. package/dist/{google-vertex-nrdd697t.js → google-vertex-tek9sqe2.js} +4 -4
  348. package/dist/{mistral-conversations-059y51zt.js → mistral-conversations-rjxa9s1j.js} +3 -3
  349. package/dist/{openai-completions-663yncfq.js → openai-completions-44gb0zsk.js} +12 -12
@@ -0,0 +1,653 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
11
+ import { createServer, type Server } from "node:http";
12
+ import WebSocket from "ws";
13
+ import {
14
+ acceptTask,
15
+ assignUnassignedTaskPending,
16
+ cancelTask,
17
+ closeDb,
18
+ createAgent,
19
+ createChannel,
20
+ createSteeringMessage,
21
+ createTaskExtended,
22
+ deleteSwarmConfig,
23
+ deleteSwarmConfigByKey,
24
+ getDbClient,
25
+ initDb,
26
+ postMessage,
27
+ startTask,
28
+ upsertSwarmConfig,
29
+ } from "../be/db";
30
+ import { startWorkerNudgeListener } from "../be/worker-nudge-listener";
31
+ import { publishWorkerNudge } from "../be/worker-nudges";
32
+ import { realtimeBus } from "../realtime/bus";
33
+ import {
34
+ addWorkerSubscription,
35
+ channelTopic,
36
+ listSubscribedWorkerAgents,
37
+ removeWorkerSubscription,
38
+ } from "../realtime/channels";
39
+ import { attachRealtimeTransport } from "../realtime/transport";
40
+ import { type WorkerNudge, WorkerNudgeSchema } from "../types";
41
+ import { getApiKey, setApiKey } from "../utils/api-key";
42
+ import { workflowEventBus } from "../workflows/event-bus";
43
+ import { listenOnFreePort } from "./test-net";
44
+
45
+ type Frame = { id?: number; type: string; seq: number; data?: unknown; error?: string };
46
+ type Client = {
47
+ agentId: string;
48
+ ws: WebSocket;
49
+ nudges: WorkerNudge[];
50
+ request: (op: string, namespace?: string, name?: string) => Promise<Frame>;
51
+ };
52
+
53
+ let server: Server;
54
+ let detach: () => void;
55
+ let socketUrl: string;
56
+ let x: string;
57
+ let y: string;
58
+ let clientX: Client;
59
+ let clientY: Client;
60
+ const clients: Client[] = [];
61
+ const previousFlag = process.env.RUNTIME_POOLS_ENABLED;
62
+ const previousEnv = { ...process.env };
63
+ let stopWorkerNudges: () => void;
64
+
65
+ beforeAll(() => {
66
+ stopWorkerNudges = startWorkerNudgeListener();
67
+ });
68
+ afterAll(() => stopWorkerNudges());
69
+
70
+ async function waitUntil(predicate: () => boolean): Promise<void> {
71
+ const deadline = Date.now() + 5_000;
72
+ while (!predicate()) {
73
+ if (Date.now() >= deadline) throw new Error("Worker nudge did not arrive within five seconds");
74
+ await Bun.sleep(5);
75
+ }
76
+ }
77
+
78
+ async function flushDbHooks(): Promise<void> {
79
+ // The FIFO hook barrier dispatches prior hooks, then the query drains their task reads.
80
+ await new Promise<void>((resolve) => getDbClient().afterCommit(resolve));
81
+ await getDbClient().get("SELECT 1");
82
+ }
83
+
84
+ async function createCommittedTask(...args: Parameters<typeof createTaskExtended>) {
85
+ const emitted = new Set<string>();
86
+ const onCreated = (data: unknown) => emitted.add((data as { taskId: string }).taskId);
87
+ workflowEventBus.on("task.created", onCreated);
88
+ try {
89
+ const task = await createTaskExtended(...args);
90
+ await waitUntil(() => emitted.has(task.id));
91
+ await flushDbHooks();
92
+ return task;
93
+ } finally {
94
+ workflowEventBus.off("task.created", onCreated);
95
+ }
96
+ }
97
+
98
+ async function socketBarrier(client: Client): Promise<void> {
99
+ const sentinel: WorkerNudge = {
100
+ v: 1,
101
+ kind: "config",
102
+ taskId: crypto.randomUUID(),
103
+ at: new Date().toISOString(),
104
+ };
105
+ realtimeBus.publish(channelTopic(`worker:${client.agentId}`, "nudge"), sentinel);
106
+ await waitUntil(() => client.nudges.some((nudge) => nudge.taskId === sentinel.taskId));
107
+ expect(client.nudges.at(-1)).toEqual(sentinel);
108
+ client.nudges.pop();
109
+ }
110
+
111
+ async function openClient(agentId: string, runtimeId = crypto.randomUUID()): Promise<Client> {
112
+ const ws = new WebSocket(socketUrl, {
113
+ headers: {
114
+ Authorization: `Bearer ${getApiKey()}`,
115
+ "X-Agent-ID": agentId,
116
+ "X-Runtime-Instance-ID": runtimeId,
117
+ },
118
+ });
119
+ let requestId = 0;
120
+ const frames: Frame[] = [];
121
+ const nudges: WorkerNudge[] = [];
122
+ ws.on("message", (raw) => {
123
+ const frame = JSON.parse(raw.toString()) as Frame;
124
+ ws.send(JSON.stringify({ ack: frame.seq }));
125
+ frames.push(frame);
126
+ if (frame.type === "channel") nudges.push(WorkerNudgeSchema.parse(frame.data));
127
+ });
128
+ const client: Client = {
129
+ agentId,
130
+ ws,
131
+ nudges,
132
+ async request(op, namespace = `worker:${agentId}`, name = "nudge") {
133
+ const id = ++requestId;
134
+ ws.send(JSON.stringify({ id, op, namespace, name }));
135
+ await waitUntil(() => frames.some((frame) => frame.id === id));
136
+ return frames.find((frame) => frame.id === id)!;
137
+ },
138
+ };
139
+ clients.push(client);
140
+ await new Promise<void>((resolve, reject) => {
141
+ ws.once("open", resolve);
142
+ ws.once("error", reject);
143
+ });
144
+ return client;
145
+ }
146
+
147
+ async function closeClient(client: Client): Promise<void> {
148
+ if (client.ws.readyState === WebSocket.CLOSED) return;
149
+ await new Promise<void>((resolve) => {
150
+ client.ws.once("close", resolve);
151
+ client.ws.close();
152
+ });
153
+ }
154
+
155
+ async function expectNudge(
156
+ client: Client,
157
+ write: () => Promise<unknown>,
158
+ kind: WorkerNudge["kind"],
159
+ taskId?: string | (() => string),
160
+ ): Promise<void> {
161
+ const count = client.nudges.length;
162
+ await write();
163
+ await waitUntil(() => client.nudges.length > count);
164
+ await flushDbHooks();
165
+ await socketBarrier(client);
166
+ const expectedTaskId = typeof taskId === "function" ? taskId() : taskId;
167
+ expect(client.nudges.slice(count)).toEqual([
168
+ { v: 1, kind, ...(expectedTaskId ? { taskId: expectedTaskId } : {}), at: expect.any(String) },
169
+ ]);
170
+ }
171
+
172
+ describe("worker nudges", () => {
173
+ beforeAll(async () => {
174
+ setApiKey(`worker-nudge-test-${crypto.randomUUID()}`);
175
+ server = createServer();
176
+ detach = attachRealtimeTransport(server);
177
+ const port = await listenOnFreePort(server);
178
+ socketUrl = `ws://127.0.0.1:${port}/api/realtime`;
179
+ });
180
+
181
+ beforeEach(async () => {
182
+ process.env.RUNTIME_POOLS_ENABLED = "true";
183
+ initDb(":memory:");
184
+ x = (await createAgent({ name: "nudge-x", isLead: false, status: "idle", capabilities: [] }))
185
+ .id;
186
+ y = (await createAgent({ name: "nudge-y", isLead: false, status: "idle", capabilities: [] }))
187
+ .id;
188
+ clientX = await openClient(x);
189
+ clientY = await openClient(y);
190
+ expect((await clientX.request("subscribe")).type).toBe("result");
191
+ expect((await clientY.request("subscribe")).type).toBe("result");
192
+ });
193
+
194
+ afterEach(async () => {
195
+ for (const client of clients.splice(0)) await closeClient(client);
196
+ await waitUntil(() => listSubscribedWorkerAgents().length === 0);
197
+ closeDb();
198
+ if (previousFlag === undefined) delete process.env.RUNTIME_POOLS_ENABLED;
199
+ else process.env.RUNTIME_POOLS_ENABLED = previousFlag;
200
+ });
201
+
202
+ afterAll(async () => {
203
+ detach();
204
+ await new Promise<void>((resolve) => server.close(() => resolve()));
205
+ for (const key of ["AGENT_SWARM_API_KEY", "API_KEY"]) {
206
+ if (previousEnv[key] === undefined) delete process.env[key];
207
+ else process.env[key] = previousEnv[key];
208
+ }
209
+ });
210
+
211
+ test("allows only the matching agent to subscribe, and rejects client writes", async () => {
212
+ expect((await clientY.request("subscribe", `worker:${x}`)).type).toBe("error");
213
+ expect((await clientX.request("subscribe", `worker:${x}`, "other")).type).toBe("error");
214
+ for (const op of ["publish", "join", "leave", "update", "change", "reset", "presence"]) {
215
+ expect((await clientX.request(op)).type).toBe("error");
216
+ }
217
+ expect((await clientY.request("unsubscribe", `worker:${x}`)).type).toBe("error");
218
+ expect(listSubscribedWorkerAgents().sort()).toEqual([x, y].sort());
219
+ });
220
+
221
+ test("nudges for pending creation, offered creation, acceptance, and pool assignment", async () => {
222
+ let pendingId = "";
223
+ await expectNudge(
224
+ clientX,
225
+ async () => {
226
+ pendingId = (await createCommittedTask("pending", { agentId: x })).id;
227
+ },
228
+ "work",
229
+ () => pendingId,
230
+ );
231
+ expect(clientX.nudges.at(-1)?.taskId).toBe(pendingId);
232
+ let offeredId = "";
233
+ const count = clientY.nudges.length;
234
+ offeredId = (await createCommittedTask("offered", { offeredTo: y })).id;
235
+ await waitUntil(() => clientY.nudges.length === count + 1);
236
+ expect(clientY.nudges.at(-1)).toMatchObject({ kind: "work", taskId: offeredId });
237
+ await expectNudge(clientY, () => acceptTask(offeredId, y), "work", offeredId);
238
+ const pool = await createCommittedTask("pool");
239
+ await expectNudge(clientX, () => assignUnassignedTaskPending(pool.id, x), "work", pool.id);
240
+ expect(await assignUnassignedTaskPending(pool.id, y)).toBeNull();
241
+ });
242
+
243
+ test("nudges cancellation of offers and deduplicates recipients", async () => {
244
+ const offered = await createCommittedTask("offered cancellation", { offeredTo: y });
245
+ await waitUntil(() => clientY.nudges.length === 1);
246
+ await expectNudge(clientY, () => cancelTask(offered.id), "cancel", offered.id);
247
+ await socketBarrier(clientX);
248
+ expect(clientX.nudges).toHaveLength(0);
249
+
250
+ const shared = await createCommittedTask("same cancellation recipient", {
251
+ agentId: x,
252
+ offeredTo: x,
253
+ });
254
+ await waitUntil(() => clientX.nudges.length === 1);
255
+ await expectNudge(clientX, () => cancelTask(shared.id), "cancel", shared.id);
256
+
257
+ const both = await createCommittedTask("two cancellation recipients", {
258
+ agentId: x,
259
+ offeredTo: y,
260
+ });
261
+ await socketBarrier(clientY);
262
+ const countX = clientX.nudges.length;
263
+ await expectNudge(clientY, () => cancelTask(both.id), "cancel", both.id);
264
+ await socketBarrier(clientX);
265
+ expect(clientX.nudges.slice(countX)).toEqual([
266
+ { v: 1, kind: "cancel", taskId: both.id, at: expect.any(String) },
267
+ ]);
268
+ });
269
+
270
+ test("nudges each unique mention, including reply mentions", async () => {
271
+ const channel = await createChannel(`nudge-${crypto.randomUUID()}`);
272
+ await expectNudge(
273
+ clientX,
274
+ () => postMessage(channel.id, y, "hello", { mentions: [x, x] }),
275
+ "work",
276
+ );
277
+ const parent = await postMessage(channel.id, y, "parent", { mentions: [x] });
278
+ await waitUntil(() => clientX.nudges.length === 2);
279
+ await expectNudge(
280
+ clientX,
281
+ () => postMessage(channel.id, y, "reply", { replyToId: parent.id }),
282
+ "work",
283
+ );
284
+ await socketBarrier(clientY);
285
+ expect(clientY.nudges).toHaveLength(0);
286
+ });
287
+
288
+ test("nudges cancellation and steering without their payloads", async () => {
289
+ const task = await createCommittedTask("active", { agentId: x });
290
+ await waitUntil(() => clientX.nudges.length === 1);
291
+ await startTask(task.id);
292
+ await expectNudge(
293
+ clientX,
294
+ () =>
295
+ createSteeringMessage({
296
+ taskId: task.id,
297
+ body: "private steering body",
298
+ mode: "queue",
299
+ source: "api",
300
+ createdByKind: "system",
301
+ }),
302
+ "steer",
303
+ task.id,
304
+ );
305
+ // Cancel another task, so steering promotion does not add a separate work nudge.
306
+ const cancelled = await createCommittedTask("cancel", { agentId: x });
307
+ await waitUntil(() => clientX.nudges.length === 3);
308
+ await expectNudge(
309
+ clientX,
310
+ () => cancelTask(cancelled.id, "private reason"),
311
+ "cancel",
312
+ cancelled.id,
313
+ );
314
+ expect(await cancelTask(cancelled.id)).toBeNull();
315
+ await flushDbHooks();
316
+ await socketBarrier(clientY);
317
+ expect(clientY.nudges).toHaveLength(0);
318
+ });
319
+
320
+ test("nudges agent config, broadcasts global and repo config, and covers both delete paths", async () => {
321
+ let configId = "";
322
+ await expectNudge(
323
+ clientX,
324
+ async () => {
325
+ configId = (
326
+ await upsertSwarmConfig({
327
+ scope: "agent",
328
+ scopeId: x,
329
+ key: "NUDGE_TEST",
330
+ value: "private value",
331
+ })
332
+ ).id;
333
+ },
334
+ "config",
335
+ );
336
+ await socketBarrier(clientY);
337
+ expect(clientY.nudges).toHaveLength(0);
338
+ await expectNudge(clientX, () => deleteSwarmConfig(configId), "config");
339
+ await expectNudge(
340
+ clientX,
341
+ () => upsertSwarmConfig({ scope: "global", key: "NUDGE_TEST", value: "global" }),
342
+ "config",
343
+ );
344
+ await waitUntil(() => clientY.nudges.length === 1);
345
+ await expectNudge(
346
+ clientX,
347
+ () => deleteSwarmConfigByKey("global", null, "NUDGE_TEST"),
348
+ "config",
349
+ );
350
+ await waitUntil(() => clientY.nudges.length === 2);
351
+ await expectNudge(
352
+ clientX,
353
+ () =>
354
+ upsertSwarmConfig({
355
+ scope: "repo",
356
+ scopeId: crypto.randomUUID(),
357
+ key: "NUDGE_TEST",
358
+ value: "repo",
359
+ }),
360
+ "config",
361
+ );
362
+ await waitUntil(() => clientY.nudges.length === 3);
363
+ expect(await deleteSwarmConfig(configId)).toBe(false);
364
+ });
365
+
366
+ test("publishes only after commit and drops hooks on rollback", async () => {
367
+ let taskId = "";
368
+ await getDbClient().transaction(async () => {
369
+ taskId = (await createTaskExtended("transaction", { agentId: x })).id;
370
+ await upsertSwarmConfig({ scope: "agent", scopeId: x, key: "NUDGE_TX", value: "one" });
371
+ await socketBarrier(clientX);
372
+ expect(clientX.nudges).toHaveLength(0);
373
+ });
374
+ await waitUntil(() => clientX.nudges.length === 2);
375
+ expect(clientX.nudges.map((nudge) => nudge.kind).sort()).toEqual(["config", "work"]);
376
+ clientX.nudges.length = 0;
377
+ await expect(
378
+ getDbClient().transaction(async () => {
379
+ await createTaskExtended("rollback", { agentId: x });
380
+ await cancelTask(taskId);
381
+ await createSteeringMessage({
382
+ taskId,
383
+ body: "rollback",
384
+ mode: "queue",
385
+ source: "api",
386
+ createdByKind: "system",
387
+ });
388
+ await upsertSwarmConfig({ scope: "global", key: "NUDGE_TX", value: "two" });
389
+ throw new Error("rollback");
390
+ }),
391
+ ).rejects.toThrow("rollback");
392
+ await flushDbHooks();
393
+ await socketBarrier(clientX);
394
+ await socketBarrier(clientY);
395
+ expect(clientX.nudges).toHaveLength(0);
396
+ expect(clientY.nudges).toHaveLength(0);
397
+ });
398
+
399
+ test("flag off suppresses every nudge and rejects new subscriptions", async () => {
400
+ process.env.RUNTIME_POOLS_ENABLED = "false";
401
+ const task = await createCommittedTask("flag off", { agentId: x });
402
+ await cancelTask(task.id);
403
+ await upsertSwarmConfig({ scope: "global", key: "NUDGE_OFF", value: "one" });
404
+ publishWorkerNudge(x, { kind: "work" });
405
+ expect((await clientX.request("subscribe")).type).toBe("error");
406
+ await flushDbHooks();
407
+ await socketBarrier(clientX);
408
+ await socketBarrier(clientY);
409
+ expect(clientX.nudges).toHaveLength(0);
410
+ expect(clientY.nudges).toHaveLength(0);
411
+ });
412
+
413
+ test("tracks each runtime socket and emits connection events", async () => {
414
+ const runtimeId = crypto.randomUUID();
415
+ const connected: unknown[] = [];
416
+ const disconnected: unknown[] = [];
417
+ const stopConnected = realtimeBus.subscribe("worker.socket.connected", (data) =>
418
+ connected.push(data),
419
+ );
420
+ const stopDisconnected = realtimeBus.subscribe("worker.socket.disconnected", (data) =>
421
+ disconnected.push(data),
422
+ );
423
+ try {
424
+ const sibling = await openClient(x, runtimeId);
425
+ await sibling.request("subscribe");
426
+ await sibling.request("subscribe");
427
+ expect(connected).toEqual([{ agentId: x, runtimeId }]);
428
+ await closeClient(sibling);
429
+ await waitUntil(() => disconnected.length === 1);
430
+ expect(disconnected).toEqual([{ agentId: x, runtimeId }]);
431
+ expect(listSubscribedWorkerAgents()).toContain(x);
432
+ await expectNudge(
433
+ clientX,
434
+ () => upsertSwarmConfig({ scope: "global", key: "NUDGE_SIBLING", value: "one" }),
435
+ "config",
436
+ );
437
+ await clientX.request("unsubscribe");
438
+ expect(listSubscribedWorkerAgents()).not.toContain(x);
439
+ expect(realtimeBus.subscriberCount(channelTopic(`worker:${x}`, "nudge"))).toBe(0);
440
+ } finally {
441
+ stopConnected();
442
+ stopDisconnected();
443
+ }
444
+ });
445
+
446
+ test.each([
447
+ 1, 2,
448
+ ])("pairs events across %i subscribe-unsubscribe cycles and close", async (cycles) => {
449
+ const runtimeId = crypto.randomUUID();
450
+ const events: { kind: string; data: unknown }[] = [];
451
+ const stopConnected = realtimeBus.subscribe("worker.socket.connected", (data) =>
452
+ events.push({ kind: "connected", data }),
453
+ );
454
+ const stopDisconnected = realtimeBus.subscribe("worker.socket.disconnected", (data) =>
455
+ events.push({ kind: "disconnected", data }),
456
+ );
457
+ try {
458
+ const sibling = await openClient(x, runtimeId);
459
+ const expected = [];
460
+ for (let i = 0; i < cycles; i++) {
461
+ await sibling.request("subscribe");
462
+ expected.push({ kind: "connected", data: { agentId: x, runtimeId } });
463
+ if (i === 0) {
464
+ await sibling.request("unsubscribe");
465
+ await sibling.request("unsubscribe");
466
+ expected.push({ kind: "disconnected", data: { agentId: x, runtimeId } });
467
+ }
468
+ }
469
+ if (cycles === 2) {
470
+ // A flag change must not suppress the disconnect paired with this subscription.
471
+ process.env.RUNTIME_POOLS_ENABLED = "false";
472
+ expected.push({ kind: "disconnected", data: { agentId: x, runtimeId } });
473
+ }
474
+ await closeClient(sibling);
475
+ if (cycles === 2) await waitUntil(() => events.length === expected.length);
476
+ process.env.RUNTIME_POOLS_ENABLED = "true";
477
+ await clientX.request("subscribe");
478
+ await waitUntil(() => events.length === expected.length);
479
+ expect(events).toEqual(expected);
480
+ } finally {
481
+ stopConnected();
482
+ stopDisconnected();
483
+ }
484
+ });
485
+ });
486
+
487
+ describe("worker nudge database hooks", () => {
488
+ let agentId: string;
489
+ let nudges: WorkerNudge[];
490
+ let stop: () => void;
491
+ const subscriber = {};
492
+
493
+ beforeEach(async () => {
494
+ process.env.RUNTIME_POOLS_ENABLED = "true";
495
+ initDb(":memory:");
496
+ agentId = (
497
+ await createAgent({ name: "nudge-db", isLead: false, status: "idle", capabilities: [] })
498
+ ).id;
499
+ nudges = [];
500
+ addWorkerSubscription(agentId, subscriber);
501
+ stop = realtimeBus.subscribe(channelTopic(`worker:${agentId}`, "nudge"), (data) => {
502
+ nudges.push(WorkerNudgeSchema.parse(data));
503
+ });
504
+ });
505
+
506
+ afterEach(async () => {
507
+ await flushDbHooks();
508
+ stop();
509
+ removeWorkerSubscription(agentId, subscriber);
510
+ closeDb();
511
+ if (previousFlag === undefined) delete process.env.RUNTIME_POOLS_ENABLED;
512
+ else process.env.RUNTIME_POOLS_ENABLED = previousFlag;
513
+ });
514
+
515
+ test("publishes one envelope for each committed write", async () => {
516
+ const pending = await createCommittedTask("pending", { agentId });
517
+ await waitUntil(() => nudges.length === 1);
518
+ expect(nudges.at(-1)).toMatchObject({ kind: "work", taskId: pending.id });
519
+ const offered = await createCommittedTask("offered", { offeredTo: agentId });
520
+ await waitUntil(() => nudges.length === 2);
521
+ expect(nudges.at(-1)).toMatchObject({ kind: "work", taskId: offered.id });
522
+ await acceptTask(offered.id, agentId);
523
+ await waitUntil(() => nudges.length === 3);
524
+ const pool = await createCommittedTask("pool");
525
+ await assignUnassignedTaskPending(pool.id, agentId);
526
+ await waitUntil(() => nudges.length === 4);
527
+ const channel = await createChannel(`db-${crypto.randomUUID()}`);
528
+ await postMessage(channel.id, null, "mention", { mentions: [agentId, agentId] });
529
+ await waitUntil(() => nudges.length === 5);
530
+ await cancelTask(pool.id);
531
+ await waitUntil(() => nudges.length === 6);
532
+ await createSteeringMessage({
533
+ taskId: pending.id,
534
+ body: "private",
535
+ mode: "queue",
536
+ source: "api",
537
+ createdByKind: "system",
538
+ });
539
+ await waitUntil(() => nudges.length === 7);
540
+ const config = await upsertSwarmConfig({
541
+ scope: "agent",
542
+ scopeId: agentId,
543
+ key: "NUDGE_DB",
544
+ value: "private",
545
+ });
546
+ await waitUntil(() => nudges.length === 8);
547
+ await deleteSwarmConfig(config.id);
548
+ await waitUntil(() => nudges.length === 9);
549
+ await flushDbHooks();
550
+ expect(nudges.map((nudge) => nudge.kind)).toEqual([
551
+ "work",
552
+ "work",
553
+ "work",
554
+ "work",
555
+ "work",
556
+ "cancel",
557
+ "steer",
558
+ "config",
559
+ "config",
560
+ ]);
561
+ for (const nudge of nudges)
562
+ expect(Object.keys(nudge).sort()).toEqual(
563
+ nudge.taskId ? ["at", "kind", "taskId", "v"] : ["at", "kind", "v"],
564
+ );
565
+ });
566
+
567
+ test("defers until commit and suppresses every rolled-back hook", async () => {
568
+ const pending = await createCommittedTask("pending", { agentId });
569
+ const offered = await createCommittedTask("offered", { offeredTo: agentId });
570
+ const pool = await createCommittedTask("pool");
571
+ const channel = await createChannel(`rollback-${crypto.randomUUID()}`);
572
+ const config = await upsertSwarmConfig({
573
+ scope: "agent",
574
+ scopeId: agentId,
575
+ key: "NUDGE_DELETE",
576
+ value: "one",
577
+ });
578
+ await waitUntil(() => nudges.length === 3);
579
+ nudges.length = 0;
580
+ await expect(
581
+ getDbClient().transaction(async () => {
582
+ await createTaskExtended("rollback", { agentId });
583
+ await acceptTask(offered.id, agentId);
584
+ await assignUnassignedTaskPending(pool.id, agentId);
585
+ await postMessage(channel.id, null, "rollback", { mentions: [agentId] });
586
+ await cancelTask(pending.id);
587
+ await createSteeringMessage({
588
+ taskId: pending.id,
589
+ body: "rollback",
590
+ mode: "queue",
591
+ source: "api",
592
+ createdByKind: "system",
593
+ });
594
+ await upsertSwarmConfig({
595
+ scope: "agent",
596
+ scopeId: agentId,
597
+ key: "NUDGE_TX",
598
+ value: "two",
599
+ });
600
+ await deleteSwarmConfig(config.id);
601
+ await getDbClient().get("SELECT 1");
602
+ expect(nudges).toHaveLength(0);
603
+ throw new Error("rollback");
604
+ }),
605
+ ).rejects.toThrow("rollback");
606
+ await flushDbHooks();
607
+ expect(nudges).toHaveLength(0);
608
+ await getDbClient().transaction(async () => {
609
+ await acceptTask(offered.id, agentId);
610
+ await upsertSwarmConfig({
611
+ scope: "agent",
612
+ scopeId: agentId,
613
+ key: "NUDGE_TX",
614
+ value: "committed",
615
+ });
616
+ await getDbClient().get("SELECT 1");
617
+ expect(nudges).toHaveLength(0);
618
+ });
619
+ await waitUntil(() => nudges.length === 2);
620
+ expect(nudges.map((nudge) => nudge.kind)).toEqual(["work", "config"]);
621
+ });
622
+
623
+ test("flag off suppresses write hooks", async () => {
624
+ process.env.RUNTIME_POOLS_ENABLED = "false";
625
+ const task = await createCommittedTask("off", { agentId });
626
+ await cancelTask(task.id);
627
+ await createSteeringMessage({
628
+ taskId: task.id,
629
+ body: "off",
630
+ mode: "queue",
631
+ source: "api",
632
+ createdByKind: "system",
633
+ });
634
+ await upsertSwarmConfig({ scope: "agent", scopeId: agentId, key: "NUDGE_OFF", value: "off" });
635
+ await flushDbHooks();
636
+ expect(nudges).toHaveLength(0);
637
+ });
638
+
639
+ test("skips task reads without subscribers or with the flag off", () => {
640
+ removeWorkerSubscription(agentId, subscriber);
641
+ const read = spyOn(getDbClient(), "get");
642
+ try {
643
+ workflowEventBus.emit("task.created", { taskId: crypto.randomUUID() });
644
+ expect(read).not.toHaveBeenCalled();
645
+ addWorkerSubscription(agentId, subscriber);
646
+ process.env.RUNTIME_POOLS_ENABLED = "false";
647
+ workflowEventBus.emit("task.created", { taskId: crypto.randomUUID() });
648
+ expect(read).not.toHaveBeenCalled();
649
+ } finally {
650
+ read.mockRestore();
651
+ }
652
+ });
653
+ });