@desplega.ai/agent-swarm 2.4.4 → 2.5.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 (382) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
  4. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
  35. package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
  37. package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
  38. package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
  44. package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  49. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  50. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  51. package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
  52. package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
  53. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  54. package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
  55. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  56. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  57. package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
  58. package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
  59. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  60. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  61. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  62. package/dist/cli-pkaqk57w.js +128 -0
  63. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  64. package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
  65. package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
  66. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  67. package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
  68. package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
  69. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  70. package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
  71. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  72. package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
  73. package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
  74. package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
  75. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  76. package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
  77. package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
  78. package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
  79. package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
  80. package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
  81. package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
  82. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  83. package/dist/cli.js +31 -29
  84. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  85. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
  86. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  87. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
  88. package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
  89. package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
  90. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
  91. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
  92. package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
  93. package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
  94. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  95. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
  96. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  97. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
  98. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  99. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  100. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  101. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  102. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  103. package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
  104. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  105. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  106. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
  107. package/dist/handlers-c9ajzhbn.js +41 -0
  108. package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
  109. package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
  110. package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
  111. package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
  112. package/dist/index-07ef7nkm.js +42 -0
  113. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  114. package/dist/index-7yts8n9s.js +43 -0
  115. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  116. package/dist/index-q3t40ryv.js +46 -0
  117. package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
  118. package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
  119. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  120. package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
  121. package/dist/lead-aayq64jn.js +79 -0
  122. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  123. package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
  124. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  125. package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
  126. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  127. package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
  128. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  129. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  130. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  131. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  132. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  133. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  134. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
  135. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  136. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  137. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
  138. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
  139. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
  140. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  141. package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
  142. package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
  143. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  144. package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
  145. package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
  146. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  147. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  148. package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
  149. package/dist/worker-nwb789mg.js +79 -0
  150. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  151. package/openapi.json +793 -6
  152. package/package.json +11 -11
  153. package/src/apps/catalog.generated.json +206 -4
  154. package/src/apps/page-validator.ts +13 -1
  155. package/src/assets/key.ts +17 -0
  156. package/src/azure-devops/handlers.ts +1 -1
  157. package/src/be/chunking.ts +42 -9
  158. package/src/be/db/approvals.ts +18 -0
  159. package/src/be/db/session-moves.ts +164 -0
  160. package/src/be/db/session-tree.ts +384 -0
  161. package/src/be/db/tasks/draft-text.ts +15 -0
  162. package/src/be/db/tasks/read.ts +54 -5
  163. package/src/be/db/workflow-runs.ts +96 -0
  164. package/src/be/db.ts +85 -237
  165. package/src/be/memory/index-content.ts +36 -17
  166. package/src/be/memory/providers/openai-embedding.ts +115 -61
  167. package/src/be/memory/providers/sqlite-store.ts +69 -23
  168. package/src/be/memory/reranker.ts +31 -1
  169. package/src/be/memory/search-candidates.ts +47 -0
  170. package/src/be/memory/types.ts +14 -0
  171. package/src/be/onboarding.ts +3 -0
  172. package/src/be/scripts/typecheck.ts +2 -0
  173. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  174. package/src/be/seed-scripts/index.ts +2 -2
  175. package/src/be/skill-sync.ts +43 -2
  176. package/src/be/swarm-config-guard.ts +25 -1
  177. package/src/be/task-attachment-store.ts +30 -0
  178. package/src/bitbucket/api.ts +81 -0
  179. package/src/bitbucket/auth.ts +64 -0
  180. package/src/bitbucket/handlers.ts +434 -0
  181. package/src/bitbucket/index.ts +28 -0
  182. package/src/bitbucket/templates.ts +134 -0
  183. package/src/bitbucket/types.ts +65 -0
  184. package/src/commands/runner.ts +55 -3
  185. package/src/e2b/env.ts +1 -0
  186. package/src/extensions/catalog.generated.json +4 -4
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/contract.ts +9 -2
  189. package/src/extensions/post-bridge.ts +1 -0
  190. package/src/fs/provider.ts +13 -1
  191. package/src/github/handlers.ts +6 -6
  192. package/src/gitlab/handlers.ts +5 -5
  193. package/src/heartbeat/heartbeat.ts +31 -11
  194. package/src/http/assets.ts +14 -2
  195. package/src/http/core.ts +12 -0
  196. package/src/http/fs.ts +54 -34
  197. package/src/http/index.ts +4 -0
  198. package/src/http/mcp-body.ts +37 -0
  199. package/src/http/mcp-user.ts +3 -5
  200. package/src/http/mcp.ts +6 -7
  201. package/src/http/memory.ts +26 -15
  202. package/src/http/onboarding.ts +1 -0
  203. package/src/http/page-proxy.ts +61 -5
  204. package/src/http/pages-public.ts +27 -44
  205. package/src/http/pages.ts +4 -4
  206. package/src/http/script-runs.ts +2 -0
  207. package/src/http/sessions.ts +366 -12
  208. package/src/http/skills.ts +48 -1
  209. package/src/http/tasks.ts +1 -1
  210. package/src/http/users.ts +13 -1
  211. package/src/http/webhooks.ts +141 -0
  212. package/src/integrations/chat-sdk/adapters.ts +24 -4
  213. package/src/integrations/chat-sdk/attachments.ts +90 -12
  214. package/src/integrations/chat-sdk/facts.ts +138 -0
  215. package/src/integrations/chat-sdk/inbound.ts +156 -8
  216. package/src/integrations/chat-sdk/limits.ts +24 -0
  217. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  218. package/src/integrations/chat-sdk/outbound.ts +272 -5
  219. package/src/integrations/chat-sdk/relay.ts +4 -1
  220. package/src/integrations/chat-sdk/router.ts +28 -2
  221. package/src/integrations/chat-sdk/steering.ts +9 -0
  222. package/src/integrations/chat-sdk/templates.ts +14 -0
  223. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  224. package/src/mcp-limits.ts +20 -0
  225. package/src/pages/browser-sdk.ts +8 -3
  226. package/src/pages/feedback-overlay.ts +1 -1
  227. package/src/providers/acp-targets.ts +1 -1
  228. package/src/providers/amp-adapter.ts +1 -1
  229. package/src/providers/codex-adapter.ts +106 -119
  230. package/src/providers/codex-agents-md.ts +35 -229
  231. package/src/providers/cursor-adapter.ts +1 -1
  232. package/src/providers/pi-mono-adapter.ts +5 -53
  233. package/src/realtime/transport.ts +1 -2
  234. package/src/script-workflows/workflow-ctx.ts +1 -0
  235. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  236. package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
  237. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  238. package/src/server-user.ts +2 -0
  239. package/src/server.ts +2 -0
  240. package/src/slack/assistant.ts +2 -2
  241. package/src/slack/files.ts +2 -0
  242. package/src/slack/handlers.test.ts +158 -0
  243. package/src/slack/handlers.ts +23 -32
  244. package/src/slack/inbound-files.ts +39 -6
  245. package/src/slack/thread-buffer.ts +2 -1
  246. package/src/tasks/context-key.ts +61 -1
  247. package/src/tasks/task-terminal-effects.ts +21 -17
  248. package/src/tests/apps-card-grid.test.ts +197 -0
  249. package/src/tests/apps-catalog-additions.test.ts +120 -0
  250. package/src/tests/apps-tree.test.ts +129 -0
  251. package/src/tests/bitbucket-auth.test.ts +156 -0
  252. package/src/tests/bitbucket-handlers.test.ts +425 -0
  253. package/src/tests/bitbucket-webhook.test.ts +220 -0
  254. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  255. package/src/tests/chat-sdk-config.test.ts +11 -0
  256. package/src/tests/chat-sdk-facts.test.ts +23 -0
  257. package/src/tests/chat-sdk-inbound.test.ts +597 -4
  258. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  259. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  260. package/src/tests/codex-adapter.test.ts +167 -242
  261. package/src/tests/config-api-only-keys.test.ts +9 -0
  262. package/src/tests/context-key.test.ts +50 -0
  263. package/src/tests/core-auth.test.ts +5 -0
  264. package/src/tests/cors-allowlist.test.ts +1 -1
  265. package/src/tests/db-task-read-contract.test.ts +7 -0
  266. package/src/tests/db-task-write-contract.test.ts +6 -0
  267. package/src/tests/docs-index-footprint.test.js +3 -3
  268. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  269. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  270. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  271. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  272. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  273. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  274. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  275. package/src/tests/generate-mcp-docs.test.ts +101 -0
  276. package/src/tests/gitlab-handlers.test.ts +3 -3
  277. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  278. package/src/tests/heartbeat-checklist.test.ts +33 -0
  279. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  280. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  281. package/src/tests/integration-disable-flags.test.ts +36 -0
  282. package/src/tests/kv-page-proxy.test.ts +91 -5
  283. package/src/tests/launch-password-rejection.test.ts +2 -2
  284. package/src/tests/mcp-input-caps.test.ts +294 -0
  285. package/src/tests/mcp-tools.test.ts +1 -1
  286. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  287. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  288. package/src/tests/memory.test.ts +14 -0
  289. package/src/tests/onboarding.test.ts +12 -0
  290. package/src/tests/page-guest-session.test.ts +19 -8
  291. package/src/tests/page-proxy-authed.test.ts +1 -1
  292. package/src/tests/page-proxy.test.ts +17 -10
  293. package/src/tests/page-session.test.ts +102 -1
  294. package/src/tests/pages-authed-mode.test.ts +82 -12
  295. package/src/tests/pages-password-mode.test.ts +5 -5
  296. package/src/tests/pi-codemode.test.ts +8 -2
  297. package/src/tests/preload-env.test.ts +57 -0
  298. package/src/tests/preload.ts +21 -0
  299. package/src/tests/prompt-template-remaining.test.ts +38 -0
  300. package/src/tests/realtime-transport.test.ts +13 -7
  301. package/src/tests/run-root-coverage.test.ts +39 -0
  302. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  303. package/src/tests/runner-skills-refresh.test.ts +58 -0
  304. package/src/tests/script-runs-http.test.ts +84 -0
  305. package/src/tests/scripts-http.test.ts +10 -1
  306. package/src/tests/secret-scrubber.test.ts +15 -0
  307. package/src/tests/sessions-move.test.ts +576 -0
  308. package/src/tests/sessions-tree.test.ts +493 -0
  309. package/src/tests/skill-files-http.test.ts +50 -0
  310. package/src/tests/skills-bundle.test.ts +99 -0
  311. package/src/tests/swarm-config-export-to-env.test.ts +23 -3
  312. package/src/tests/task-audio-transcription.test.ts +299 -0
  313. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  314. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  315. package/src/tests/transcription.test.ts +299 -0
  316. package/src/tests/vcs-bitbucket.test.ts +91 -0
  317. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  318. package/src/tests/vcs-provider.test.ts +16 -1
  319. package/src/tests/vcs-tracking.test.ts +3 -3
  320. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  321. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  322. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  323. package/src/tests/workflow-foreach.test.ts +44 -23
  324. package/src/tests/workflow-step-claims.test.ts +6 -6
  325. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  326. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  327. package/src/tests/workflow-tla-races.test.ts +2 -2
  328. package/src/tools/chat-access.ts +10 -0
  329. package/src/tools/chat-post.ts +31 -11
  330. package/src/tools/chat-reply.ts +32 -10
  331. package/src/tools/memory-search.ts +3 -1
  332. package/src/tools/script-delete.ts +1 -1
  333. package/src/tools/system-one-decide.ts +17 -4
  334. package/src/tools/workflows/create-workflow.ts +5 -3
  335. package/src/types.ts +9 -2
  336. package/src/utils/config-env-export.ts +4 -1
  337. package/src/utils/page-session.ts +133 -20
  338. package/src/utils/secret-scrubber.ts +4 -0
  339. package/src/utils/skills-refresh.ts +83 -43
  340. package/src/utils/transcription.ts +246 -0
  341. package/src/vcs/bitbucket.ts +58 -0
  342. package/src/vcs/index.ts +2 -0
  343. package/src/vcs/types.ts +2 -2
  344. package/src/workflows/approval-resolution.ts +4 -1
  345. package/src/workflows/checkpoint.ts +9 -1
  346. package/src/workflows/constants.ts +3 -0
  347. package/src/workflows/definition.ts +48 -17
  348. package/src/workflows/engine.ts +25 -15
  349. package/src/workflows/executors/foreach.ts +45 -99
  350. package/src/workflows/executors/system-one-decision.ts +28 -7
  351. package/src/workflows/executors/system-one-providers.ts +39 -4
  352. package/src/workflows/foreach-body.ts +146 -0
  353. package/src/workflows/foreach-driver.ts +552 -0
  354. package/src/workflows/foreach-join.ts +206 -7
  355. package/src/workflows/node-timeout.ts +21 -0
  356. package/src/workflows/recovery.ts +68 -3
  357. package/src/workflows/resume.ts +221 -43
  358. package/src/workflows/retry-poller.ts +39 -24
  359. package/src/workflows/step-input.ts +59 -0
  360. package/src/workflows/task-step-routing.ts +114 -56
  361. package/templates/extensions/slack-reply-gate/README.md +24 -22
  362. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  363. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  364. package/templates/skills/apps/SKILL.md +28 -6
  365. package/templates/skills/apps/content.md +28 -6
  366. package/templates/skills/chat-sdk/content.md +6 -2
  367. package/templates/skills/code-quality/content.md +2 -1
  368. package/templates/skills/heartbeat-runbook/content.md +5 -5
  369. package/templates/skills/pages/content.md +6 -4
  370. package/templates/skills/slack-interaction/content.md +5 -0
  371. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  372. package/templates/skills/swarm-scripts/content.md +2 -2
  373. package/templates/skills/work-on-task/SKILL.md +1 -1
  374. package/templates/skills/work-on-task/content.md +1 -1
  375. package/dist/cli-51y09c6p.js +0 -2657
  376. package/dist/cli-yw91rnmw.js +0 -13
  377. package/dist/handlers-0pvee9nw.js +0 -39
  378. package/dist/index-gdh8jrdm.js +0 -43
  379. package/dist/index-hg0x2cdv.js +0 -40
  380. package/dist/index-thm0y6a8.js +0 -41
  381. package/dist/lead-ee8vazz2.js +0 -77
  382. package/dist/worker-16s75hm5.js +0 -77
@@ -0,0 +1,537 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { z } from "zod";
4
+ import * as db from "../be/db";
5
+ import {
6
+ closeDb,
7
+ completeTask,
8
+ createAgent,
9
+ createWorkflow,
10
+ failTask,
11
+ getDbClient,
12
+ getTaskByWorkflowRunStepId,
13
+ getWorkflow,
14
+ getWorkflowRun,
15
+ getWorkflowRunStepsByRunId,
16
+ initDb,
17
+ } from "../be/db";
18
+ import type { Workflow, WorkflowDefinition, WorkflowRun, WorkflowRunStep } from "../types";
19
+ import { startWorkflowExecution } from "../workflows/engine";
20
+ import { workflowEventBus } from "../workflows/event-bus";
21
+ import {
22
+ BaseExecutor,
23
+ type ExecutorDependencies,
24
+ type ExecutorResult,
25
+ } from "../workflows/executors/base";
26
+ import { createExecutorRegistry } from "../workflows/executors/registry";
27
+ import { drainForeachDrives } from "../workflows/foreach-driver";
28
+ import { retryFailedRun, setupWorkflowResumeListener } from "../workflows/resume";
29
+
30
+ const TEST_DB_PATH = "./test-workflow-foreach-retry.sqlite";
31
+
32
+ const deps: ExecutorDependencies = {
33
+ db,
34
+ eventBus: workflowEventBus,
35
+ interpolate: (t: string) => t,
36
+ };
37
+ const registry = createExecutorRegistry(deps);
38
+
39
+ /** Item ids the build step fails on; a test clears one to let its retry pass. */
40
+ const failingSpecs = new Set<string>();
41
+
42
+ /** The child workflow's one step: "builds" a spec, failing for `failingSpecs`. */
43
+ class BuildExecutor extends BaseExecutor<z.ZodObject, z.ZodObject> {
44
+ readonly type = "test-build";
45
+ readonly mode = "instant" as const;
46
+ readonly configSchema = z.object({ spec: z.unknown(), deps: z.unknown().optional() });
47
+ readonly outputSchema = z.object({ built: z.string(), deps: z.unknown().optional() });
48
+
49
+ protected async execute(config: {
50
+ spec: unknown;
51
+ deps?: unknown;
52
+ }): Promise<ExecutorResult<{ built: string; deps?: unknown }>> {
53
+ // A sub-workflow passes a non-string input value as JSON text.
54
+ const spec = parseJsonText(config.spec) as { id?: string } | undefined;
55
+ const id = spec?.id ?? "?";
56
+ if (failingSpecs.has(id)) return { status: "failed", error: `build of ${id} failed` };
57
+ const deps = config.deps === "" ? undefined : parseJsonText(config.deps);
58
+ return { status: "success", output: { built: `built-${id}`, ...(deps ? { deps } : {}) } };
59
+ }
60
+ }
61
+ registry.register(new BuildExecutor(deps));
62
+
63
+ /**
64
+ * Stands in for `swarm-script` (the scripts runtime is not booted here) and
65
+ * runs the two scripts of the documented dependency-wave example.
66
+ */
67
+ class WaveScriptExecutor extends BaseExecutor<z.ZodObject, z.ZodObject> {
68
+ readonly type = "swarm-script";
69
+ readonly mode = "instant" as const;
70
+ readonly configSchema = z.object({
71
+ scriptName: z.string(),
72
+ args: z.record(z.string(), z.unknown()).default({}),
73
+ });
74
+ readonly outputSchema = z.object({ result: z.unknown() });
75
+
76
+ protected async execute(config: {
77
+ scriptName: string;
78
+ args: Record<string, unknown>;
79
+ }): Promise<ExecutorResult<{ result: unknown }>> {
80
+ if (config.scriptName === "plan-waves") {
81
+ return {
82
+ status: "success",
83
+ output: {
84
+ result: {
85
+ wave1: [{ id: "a" }, { id: "b" }],
86
+ wave2: [{ id: "c", dependsOn: ["a", "b"] }],
87
+ },
88
+ },
89
+ };
90
+ }
91
+ // prep-wave2: attach each wave-2 job's built dependencies from wave 1.
92
+ const wave1 = config.args.wave1 as {
93
+ results: Array<{ itemKey: string; output: { outputs: { build: unknown } } }>;
94
+ };
95
+ const wave2 = config.args.wave2 as Array<{ id: string; dependsOn: string[] }>;
96
+ const done = Object.fromEntries(
97
+ wave1.results.map((result) => [result.itemKey, result.output.outputs.build]),
98
+ );
99
+ return {
100
+ status: "success",
101
+ output: {
102
+ result: wave2.map((job) => ({ ...job, deps: job.dependsOn.map((key) => done[key]) })),
103
+ },
104
+ };
105
+ }
106
+ }
107
+ registry.register(new WaveScriptExecutor(deps));
108
+
109
+ function parseJsonText(value: unknown): unknown {
110
+ if (typeof value !== "string") return value;
111
+ try {
112
+ return JSON.parse(value);
113
+ } catch {
114
+ return value;
115
+ }
116
+ }
117
+
118
+ let teardown: (() => void) | undefined;
119
+ let agentId: string;
120
+
121
+ beforeAll(async () => {
122
+ await removeDbFiles();
123
+ initDb(TEST_DB_PATH);
124
+ agentId = (await createAgent({ name: "Retrier", status: "idle" })).id;
125
+ teardown = setupWorkflowResumeListener(workflowEventBus, registry);
126
+ });
127
+
128
+ afterAll(async () => {
129
+ teardown?.();
130
+ await drainForeachDrives();
131
+ closeDb();
132
+ await removeDbFiles();
133
+ });
134
+
135
+ describe("foreach run retry with attempt rows", () => {
136
+ test("a retry reruns only the failed and cancelled items as new attempts", async () => {
137
+ const runId = await failSecondOfFour();
138
+
139
+ await retryFailedRun(runId, registry);
140
+ await drainForeachDrives();
141
+
142
+ const steps = await children(runId);
143
+ const attemptOne = steps.filter((step) => attemptOf(step) === 1).map((step) => step.nodeId);
144
+ expect(attemptOne.sort()).toEqual(["fan#i1", "fan#i2", "fan#i3"]);
145
+ expect(steps.filter((step) => step.nodeId === "fan#i0")).toHaveLength(1);
146
+
147
+ for (;;) {
148
+ const [next] = await inFlightTasks(runId);
149
+ if (!next) break;
150
+ await completeTask(next.taskId, `retry ${next.nodeId}`);
151
+ await waitForStep(next.stepId, "completed");
152
+ await drainForeachDrives();
153
+ }
154
+ const run = await waitForRun(runId, (r) => r.status === "completed");
155
+ expect(await taskCountForNode(runId, "fan#i0")).toBe(1);
156
+ const aggregate = (run.context as Record<string, unknown>).fan as {
157
+ okCount: number;
158
+ failedCount: number;
159
+ results: Array<{ itemKey: string; output: { taskOutput: unknown } }>;
160
+ };
161
+ expect(aggregate.okCount).toBe(4);
162
+ expect(aggregate.failedCount).toBe(0);
163
+ expect(aggregate.results.map((result) => result.output.taskOutput)).toEqual([
164
+ "first ok",
165
+ "retry fan#i1",
166
+ "retry fan#i2",
167
+ "retry fan#i3",
168
+ ]);
169
+ });
170
+
171
+ test("two concurrent retries add one attempt row per item", async () => {
172
+ const runId = await failSecondOfFour();
173
+ const results = await Promise.allSettled([
174
+ retryFailedRun(runId, registry),
175
+ retryFailedRun(runId, registry),
176
+ ]);
177
+ await drainForeachDrives();
178
+ expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
179
+ const steps = await children(runId);
180
+ for (const nodeId of ["fan#i1", "fan#i2", "fan#i3"]) {
181
+ expect(steps.filter((step) => step.nodeId === nodeId)).toHaveLength(2);
182
+ }
183
+ });
184
+
185
+ test("a retry that would breach the step cap is refused and the run stays failed", async () => {
186
+ const runId = await failSecondOfFour();
187
+ // 1 parent + 4 items exist; 3 attempts would make 8, and `after` needs headroom.
188
+ const previous = process.env.WORKFLOW_MAX_STEPS_PER_RUN;
189
+ process.env.WORKFLOW_MAX_STEPS_PER_RUN = "8";
190
+ try {
191
+ await expect(retryFailedRun(runId, registry)).rejects.toThrow("WORKFLOW_MAX_STEPS_PER_RUN");
192
+ } finally {
193
+ if (previous === undefined) delete process.env.WORKFLOW_MAX_STEPS_PER_RUN;
194
+ else process.env.WORKFLOW_MAX_STEPS_PER_RUN = previous;
195
+ }
196
+ expect((await getWorkflowRun(runId))?.status).toBe("failed");
197
+ expect(await children(runId)).toHaveLength(4);
198
+ });
199
+
200
+ test("a retry landing exactly on the cap of a terminal fan-out is refused and inserts nothing", async () => {
201
+ const workflow = await makeWorkflow({
202
+ nodes: [
203
+ {
204
+ id: "fan",
205
+ type: "foreach",
206
+ config: {
207
+ over: [{ id: "a" }],
208
+ itemKey: "id",
209
+ body: { type: "agent-task", config: { agentId, template: "Item {{item.id}}" } },
210
+ },
211
+ },
212
+ ],
213
+ });
214
+ const runId = await startWorkflowExecution(workflow, {}, registry);
215
+ await drainForeachDrives();
216
+ const [item] = await inFlightTasks(runId);
217
+ await failTask(item!.taskId, "first failure");
218
+ await waitForRun(runId, (r) => r.status === "failed");
219
+
220
+ // 1 parent + 1 item exist; one attempt makes 3, which the retry walk's
221
+ // circuit breaker refuses after the row is already in.
222
+ const previous = process.env.WORKFLOW_MAX_STEPS_PER_RUN;
223
+ process.env.WORKFLOW_MAX_STEPS_PER_RUN = "3";
224
+ try {
225
+ await expect(retryFailedRun(runId, registry)).rejects.toThrow("WORKFLOW_MAX_STEPS_PER_RUN");
226
+ } finally {
227
+ if (previous === undefined) delete process.env.WORKFLOW_MAX_STEPS_PER_RUN;
228
+ else process.env.WORKFLOW_MAX_STEPS_PER_RUN = previous;
229
+ }
230
+ expect((await getWorkflowRun(runId))?.status).toBe("failed");
231
+ expect((await children(runId)).map((step) => step.status)).toEqual(["failed"]);
232
+
233
+ // The failed attempt is still the latest row, so a retry under a higher cap works.
234
+ await retryFailedRun(runId, registry);
235
+ await drainForeachDrives();
236
+ const [replacement] = await inFlightTasks(runId);
237
+ await completeTask(replacement!.taskId, "ok");
238
+ await waitForRun(runId, (r) => r.status === "completed");
239
+ });
240
+
241
+ test("a sibling that fails while the retry checks readiness gets its own attempt", async () => {
242
+ const workflow = await makeWorkflow({
243
+ nodes: [
244
+ {
245
+ id: "fan",
246
+ type: "foreach",
247
+ config: {
248
+ over: [{ id: "a" }, { id: "b" }],
249
+ itemKey: "id",
250
+ failurePolicy: "fail-fast",
251
+ body: { type: "agent-task", config: { agentId, template: "Item {{item.id}}" } },
252
+ },
253
+ },
254
+ ],
255
+ });
256
+ const runId = await startWorkflowExecution(workflow, {}, registry);
257
+ await drainForeachDrives();
258
+ const [first, second] = await inFlightTasks(runId);
259
+ await failTask(first!.taskId, "a failed");
260
+ await waitForRun(runId, (r) => r.status === "failed");
261
+
262
+ // Hold the retry inside its awaited readiness check while b fails late.
263
+ const foreach = registry.get("foreach");
264
+ const checkReadiness = foreach.checkReadiness;
265
+ const entered = Promise.withResolvers<void>();
266
+ const release = Promise.withResolvers<void>();
267
+ foreach.checkReadiness = async () => {
268
+ entered.resolve();
269
+ await release.promise;
270
+ return [];
271
+ };
272
+ const retry = retryFailedRun(runId, registry);
273
+ try {
274
+ await entered.promise;
275
+ await failTask(second!.taskId, "b failed during the retry's readiness check");
276
+ await waitForStep(second!.stepId, "failed");
277
+ } finally {
278
+ foreach.checkReadiness = checkReadiness;
279
+ release.resolve();
280
+ await retry;
281
+ }
282
+ await drainForeachDrives();
283
+
284
+ const steps = await children(runId);
285
+ expect(steps.filter((step) => step.nodeId === "fan#a")).toHaveLength(2);
286
+ expect(steps.filter((step) => step.nodeId === "fan#b")).toHaveLength(2);
287
+ for (const next of await inFlightTasks(runId)) {
288
+ await completeTask(next.taskId, `retry ${next.nodeId}`);
289
+ }
290
+ const run = await waitForRun(runId, (r) => r.status === "completed");
291
+ const aggregate = (run.context as Record<string, unknown>).fan as {
292
+ okCount: number;
293
+ failedCount: number;
294
+ };
295
+ expect(aggregate.okCount).toBe(2);
296
+ expect(aggregate.failedCount).toBe(0);
297
+ });
298
+
299
+ test("a sub-workflow item retry starts a new child run and keeps the old one", async () => {
300
+ const child = await makeWorkflow(buildWorkflow());
301
+ const parent = await makeWorkflow({
302
+ nodes: [
303
+ {
304
+ id: "fan",
305
+ type: "foreach",
306
+ config: {
307
+ over: "{{trigger.items}}",
308
+ itemKey: "id",
309
+ failurePolicy: "fail-fast",
310
+ body: {
311
+ type: "sub-workflow",
312
+ config: { workflowId: child.id, inputs: { spec: "{{item}}" } },
313
+ },
314
+ },
315
+ },
316
+ ],
317
+ });
318
+ failingSpecs.add("b");
319
+ const runId = await startWorkflowExecution(
320
+ parent,
321
+ { items: [{ id: "a" }, { id: "b" }] },
322
+ registry,
323
+ );
324
+ await drainForeachDrives();
325
+ await waitForRun(runId, (r) => r.status === "failed");
326
+ const failedAttempt = (await children(runId)).find((step) => step.nodeId === "fan#b")!;
327
+ expect(failedAttempt.status).toBe("failed");
328
+
329
+ failingSpecs.delete("b");
330
+ await retryFailedRun(runId, registry);
331
+ await drainForeachDrives();
332
+ await waitForRun(runId, (r) => r.status === "completed");
333
+
334
+ const attempts = (await children(runId)).filter((step) => step.nodeId === "fan#b");
335
+ expect(attempts.map((step) => step.status)).toEqual(["failed", "completed"]);
336
+ const childRuns = await getDbClient().query<{ parentStepId: string; status: string }>(
337
+ "SELECT parentStepId, status FROM workflow_runs WHERE workflowId = ? ORDER BY startedAt",
338
+ [child.id],
339
+ );
340
+ const forB = childRuns.filter((row) => attempts.some((step) => step.id === row.parentStepId));
341
+ expect(forB).toHaveLength(2);
342
+ expect(forB.find((row) => row.parentStepId === failedAttempt.id)?.status).toBe("failed");
343
+ expect(forB.find((row) => row.parentStepId === attempts[1]!.id)?.status).toBe("completed");
344
+ });
345
+ });
346
+
347
+ describe("dependency waves", () => {
348
+ test("two caller-composed waves run end to end without changing the definition", async () => {
349
+ const child = await makeWorkflow(buildWorkflow());
350
+ const workflow = await makeWorkflow(twoWaveDefinition(child.id));
351
+ const before = await getWorkflow(workflow.id);
352
+
353
+ const runId = await startWorkflowExecution(workflow, {}, registry);
354
+ await drainForeachDrives();
355
+ const run = await waitForRun(runId, (r) => r.status === "completed");
356
+
357
+ const wave2 = (run.context as Record<string, unknown>).wave2 as {
358
+ okCount: number;
359
+ results: Array<{ itemKey: string; output: { outputs: { build: { deps: unknown } } } }>;
360
+ };
361
+ expect(wave2.okCount).toBe(1);
362
+ expect(wave2.results[0]?.output.outputs.build.deps).toEqual([
363
+ { built: "built-a" },
364
+ { built: "built-b" },
365
+ ]);
366
+
367
+ const after = await getWorkflow(workflow.id);
368
+ expect(JSON.stringify(after?.definition)).toBe(JSON.stringify(before?.definition));
369
+ expect(after?.updatedAt).toBe(before?.updatedAt);
370
+ });
371
+ });
372
+
373
+ /** The documented two-wave example (runbooks/workflows.md), with `workflowId` filled in. */
374
+ function twoWaveDefinition(buildWorkflowId: string): WorkflowDefinition {
375
+ return {
376
+ nodes: [
377
+ { id: "plan", type: "swarm-script", config: { scriptName: "plan-waves" }, next: "wave1" },
378
+ {
379
+ id: "wave1",
380
+ type: "foreach",
381
+ inputs: { p: "plan" },
382
+ config: {
383
+ over: "{{p.result.wave1}}",
384
+ itemKey: "id",
385
+ concurrency: 3,
386
+ failurePolicy: "fail-fast",
387
+ body: {
388
+ type: "sub-workflow",
389
+ config: { workflowId: buildWorkflowId, inputs: { spec: "{{item}}" } },
390
+ },
391
+ },
392
+ next: "prep2",
393
+ },
394
+ {
395
+ id: "prep2",
396
+ type: "swarm-script",
397
+ inputs: { p: "plan", w1: "wave1" },
398
+ config: {
399
+ scriptName: "prep-wave2",
400
+ args: { wave1: "{{w1}}", wave2: "{{p.result.wave2}}" },
401
+ },
402
+ next: "wave2",
403
+ },
404
+ {
405
+ id: "wave2",
406
+ type: "foreach",
407
+ inputs: { prep: "prep2" },
408
+ config: {
409
+ over: "{{prep.result}}",
410
+ itemKey: "id",
411
+ concurrency: 3,
412
+ body: {
413
+ type: "sub-workflow",
414
+ config: {
415
+ workflowId: buildWorkflowId,
416
+ inputs: { spec: "{{item}}", deps: "{{item.deps}}" },
417
+ },
418
+ },
419
+ },
420
+ },
421
+ ],
422
+ };
423
+ }
424
+
425
+ function buildWorkflow(): WorkflowDefinition {
426
+ return {
427
+ nodes: [
428
+ {
429
+ id: "build",
430
+ type: "test-build",
431
+ config: { spec: "{{trigger.spec}}", deps: "{{trigger.deps}}" },
432
+ },
433
+ ],
434
+ };
435
+ }
436
+
437
+ /**
438
+ * A 4-item agent-task fan-out, cap 1, fail-fast: item i0 completes, i1 fails,
439
+ * i2 and i3 are cancelled before they start. Returns the failed run id.
440
+ */
441
+ async function failSecondOfFour(): Promise<string> {
442
+ const workflow = await makeWorkflow({
443
+ nodes: [
444
+ {
445
+ id: "fan",
446
+ type: "foreach",
447
+ config: {
448
+ over: "{{trigger.items}}",
449
+ itemKey: "id",
450
+ concurrency: 1,
451
+ failurePolicy: "fail-fast",
452
+ body: { type: "agent-task", config: { agentId, template: "Item {{item.id}}" } },
453
+ },
454
+ next: "after",
455
+ },
456
+ { id: "after", type: "notify", config: { channel: "swarm", template: "done" } },
457
+ ],
458
+ });
459
+ const items = ["i0", "i1", "i2", "i3"].map((id) => ({ id }));
460
+ const runId = await startWorkflowExecution(workflow, { items }, registry);
461
+ await drainForeachDrives();
462
+ const [first] = await inFlightTasks(runId);
463
+ await completeTask(first!.taskId, "first ok");
464
+ await waitForStep(first!.stepId, "completed");
465
+ await drainForeachDrives();
466
+ const [second] = await inFlightTasks(runId);
467
+ await failTask(second!.taskId, "second broke");
468
+ await waitForRun(runId, (r) => r.status === "failed");
469
+ const statuses = (await children(runId)).map((step) => step.status);
470
+ expect(statuses).toEqual(["completed", "failed", "cancelled", "cancelled"]);
471
+ return runId;
472
+ }
473
+
474
+ function makeWorkflow(definition: WorkflowDefinition): Promise<Workflow> {
475
+ return createWorkflow({ name: `foreach-retry-${crypto.randomUUID()}`, definition });
476
+ }
477
+
478
+ async function children(runId: string): Promise<WorkflowRunStep[]> {
479
+ return (await getWorkflowRunStepsByRunId(runId)).filter((step) => step.nodeId.startsWith("fan#"));
480
+ }
481
+
482
+ function attemptOf(step: WorkflowRunStep): number {
483
+ const attempt = (step.input as { attempt?: unknown } | null)?.attempt;
484
+ return typeof attempt === "number" ? attempt : 0;
485
+ }
486
+
487
+ async function inFlightTasks(
488
+ runId: string,
489
+ ): Promise<Array<{ stepId: string; nodeId: string; taskId: string }>> {
490
+ const result = [];
491
+ for (const step of await children(runId)) {
492
+ if (step.status !== "waiting" && step.status !== "running") continue;
493
+ const task = await getTaskByWorkflowRunStepId(step.id);
494
+ if (task) result.push({ stepId: step.id, nodeId: step.nodeId, taskId: task.id });
495
+ }
496
+ return result;
497
+ }
498
+
499
+ async function taskCountForNode(runId: string, nodeId: string): Promise<number> {
500
+ const row = await getDbClient().get<{ count: number }>(
501
+ `SELECT COUNT(*) AS count FROM agent_tasks t
502
+ JOIN workflow_run_steps s ON s.id = t.workflowRunStepId
503
+ WHERE s.runId = ? AND s.nodeId = ?`,
504
+ [runId, nodeId],
505
+ );
506
+ return row?.count ?? 0;
507
+ }
508
+
509
+ async function waitForStep(stepId: string, status: string): Promise<void> {
510
+ const deadline = Date.now() + 3_000;
511
+ while (Date.now() < deadline) {
512
+ if ((await db.getWorkflowRunStep(stepId))?.status === status) return;
513
+ await Bun.sleep(10);
514
+ }
515
+ throw new Error(`Timed out waiting for step ${stepId} to be ${status}`);
516
+ }
517
+
518
+ async function waitForRun(
519
+ runId: string,
520
+ done: (run: WorkflowRun) => boolean,
521
+ timeoutMs = 3_000,
522
+ ): Promise<WorkflowRun> {
523
+ const deadline = Date.now() + timeoutMs;
524
+ let run: WorkflowRun | null = null;
525
+ while (Date.now() < deadline) {
526
+ run = await getWorkflowRun(runId);
527
+ if (run && done(run)) return run;
528
+ await Bun.sleep(10);
529
+ }
530
+ throw new Error(`Timed out waiting for run ${runId}; last status ${run?.status} ${run?.error}`);
531
+ }
532
+
533
+ async function removeDbFiles(): Promise<void> {
534
+ for (const suffix of ["", "-wal", "-shm"]) {
535
+ await unlink(TEST_DB_PATH + suffix).catch(() => {});
536
+ }
537
+ }
@@ -386,16 +386,20 @@ describe("workflow foreach", () => {
386
386
  await waitFor(async () => (await getWorkflowRun(runId))?.status === "failed");
387
387
 
388
388
  await expect(retryFailedRun(runId, registry)).resolves.toBeUndefined();
389
- const replacementTask = await getTaskByWorkflowRunStepId(child.id);
389
+ // The retry is a new attempt row for the same item; the failed row stays.
390
+ const retried = (await latestChild(runId, child.nodeId))!;
391
+ expect(retried.id).not.toBe(child.id);
392
+ const replacementTask = await getTaskByWorkflowRunStepId(retried.id);
390
393
  expect(replacementTask).not.toBeNull();
391
394
  expect(replacementTask?.id).not.toBe(originalTask.id);
392
395
  expect((await getWorkflowRun(runId))?.status).toBe("waiting");
393
396
 
394
- await completeChild(runId, child.id, "retried", bus);
397
+ await completeChild(runId, retried.id, "retried", bus);
395
398
  await waitFor(async () => (await getWorkflowRun(runId))?.status === "completed");
396
399
  expect((await stepByNodeId(runId, "after"))?.status).toBe("completed");
397
400
  expect(((await getContext(runId)).reflect as { okCount: number }).okCount).toBe(1);
398
- expect((await stepById(runId, child.id))?.error).toBeUndefined();
401
+ expect((await stepById(runId, retried.id))?.error).toBeUndefined();
402
+ expect((await stepById(runId, child.id))?.status).toBe("failed");
399
403
  expect((await getWorkflowRun(runId))?.error).toBeUndefined();
400
404
  });
401
405
 
@@ -433,10 +437,11 @@ describe("workflow foreach", () => {
433
437
  });
434
438
  await Bun.sleep(20);
435
439
 
440
+ const retried = (await latestChild(runId, child.nodeId))!;
436
441
  expect((await getWorkflowRun(runId))?.status).toBe("waiting");
437
- expect((await stepById(runId, child.id))?.status).toBe("waiting");
442
+ expect((await stepById(runId, retried.id))?.status).toBe("waiting");
438
443
 
439
- await completeChild(runId, child.id, "real retry output", bus);
444
+ await completeChild(runId, retried.id, "real retry output", bus);
440
445
  await waitFor(async () => (await getWorkflowRun(runId))?.status === "completed");
441
446
  const aggregate = (await getContext(runId)).reflect as {
442
447
  results: Array<{ output: { taskOutput: string } }>;
@@ -915,7 +920,7 @@ describe("workflow foreach", () => {
915
920
  expect(deferred.valid).toBe(true);
916
921
  });
917
922
 
918
- test("definition validation rejects reserved hashes and foreach concurrency", () => {
923
+ test("definition validation rejects reserved hashes and validates foreach concurrency", () => {
919
924
  const hashResult = validateDefinition({
920
925
  nodes: [{ id: "reflect#bad", type: "record", config: { message: "bad" } }],
921
926
  });
@@ -947,24 +952,35 @@ describe("workflow foreach", () => {
947
952
  newHashId.errors.some((error) => error.includes('Node "fresh#node" contains reserved')),
948
953
  ).toBe(true);
949
954
 
950
- const concurrencyResult = validateDefinition({
951
- nodes: [
952
- {
953
- id: "reflect",
954
- type: "foreach",
955
- config: {
956
- over: [],
957
- itemKey: "id",
958
- concurrency: 2,
959
- body: { type: "agent-task", config: { template: "Reflect" } },
955
+ const withConcurrency = (concurrency: unknown) =>
956
+ validateDefinition({
957
+ nodes: [
958
+ {
959
+ id: "reflect",
960
+ type: "foreach",
961
+ config: {
962
+ over: [],
963
+ itemKey: "id",
964
+ concurrency,
965
+ body: { type: "agent-task", config: { template: "Reflect" } },
966
+ },
960
967
  },
961
- },
962
- ],
963
- });
964
- expect(concurrencyResult.valid).toBe(false);
965
- expect(concurrencyResult.errors.some((error) => error.includes("not supported in v1"))).toBe(
966
- true,
967
- );
968
+ ],
969
+ });
970
+ for (const accepted of [1, 2, 50]) {
971
+ expect(withConcurrency(accepted).valid).toBe(true);
972
+ }
973
+ for (const rejected of [0, -1, 1.5, "2"]) {
974
+ const result = withConcurrency(rejected);
975
+ expect(result.valid).toBe(false);
976
+ expect(
977
+ result.errors.some((error) =>
978
+ error.includes(
979
+ 'Node "reflect" foreach config.concurrency must be an integer of 1 or more',
980
+ ),
981
+ ),
982
+ ).toBe(true);
983
+ }
968
984
  });
969
985
 
970
986
  test("definition validation rejects foreach inside a cycle through a port edge", () => {
@@ -1055,6 +1071,11 @@ async function foreachChildren(runId: string) {
1055
1071
  );
1056
1072
  }
1057
1073
 
1074
+ /** The latest attempt row of a foreach item. */
1075
+ async function latestChild(runId: string, nodeId: string) {
1076
+ return (await getWorkflowRunStepsByRunId(runId)).filter((step) => step.nodeId === nodeId).at(-1);
1077
+ }
1078
+
1058
1079
  async function stepByNodeId(runId: string, nodeId: string) {
1059
1080
  return (await getWorkflowRunStepsByRunId(runId)).find((step) => step.nodeId === nodeId);
1060
1081
  }
@@ -74,23 +74,23 @@ describe("failStepAndRunIfWaiting", () => {
74
74
  // The live task.failed handler and the recovery sweep both saw `waiting`
75
75
  // before their awaits; only the transactional re-read may win.
76
76
  const [a, b] = await Promise.all([
77
- failStepAndRunIfWaiting(stepId, runId, "live event"),
78
- failStepAndRunIfWaiting(stepId, runId, "recovery sweep"),
77
+ failStepAndRunIfWaiting(def, stepId, runId, "live event"),
78
+ failStepAndRunIfWaiting(def, stepId, runId, "recovery sweep"),
79
79
  ]);
80
- expect([a, b].filter(Boolean).length).toBe(1);
80
+ expect([a.claimed, b.claimed].filter(Boolean).length).toBe(1);
81
81
  const step = await getWorkflowRunStep(stepId);
82
82
  const run = await getWorkflowRun(runId);
83
83
  expect(step?.status).toBe("failed");
84
84
  expect(run?.status).toBe("failed");
85
85
  // The loser must not have overwritten the winner's reason.
86
- expect(step?.error).toBe(a ? "live event" : "recovery sweep");
86
+ expect(step?.error).toBe(a.claimed ? "live event" : "recovery sweep");
87
87
  });
88
88
 
89
89
  test("does not stomp a step another handler already routed", async () => {
90
90
  const { runId, stepId } = await createWaitingStep();
91
91
  await updateWorkflowRunStep(stepId, { status: "completed" });
92
92
  await updateWorkflowRun(runId, { status: "running" });
93
- expect(await failStepAndRunIfWaiting(stepId, runId, "stale sweep")).toBe(false);
93
+ expect((await failStepAndRunIfWaiting(def, stepId, runId, "stale sweep")).claimed).toBe(false);
94
94
  expect((await getWorkflowRunStep(stepId))?.status).toBe("completed");
95
95
  expect((await getWorkflowRun(runId))?.status).toBe("running");
96
96
  });
@@ -145,7 +145,7 @@ describe("checkpointPortStepAndResolveSuccessors", () => {
145
145
  );
146
146
  expect(routing.claimed).toBe(true);
147
147
  expect(routing.successors.map((n) => n.id)).toEqual(["after-reject"]);
148
- expect(await failStepAndRunIfWaiting(stepId, runId, "late failure")).toBe(false);
148
+ expect((await failStepAndRunIfWaiting(def, stepId, runId, "late failure")).claimed).toBe(false);
149
149
  expect((await getWorkflowRunStep(stepId))?.status).toBe("completed");
150
150
  expect((await getWorkflowRun(runId))?.status).toBe("running");
151
151
  });