@desplega.ai/agent-swarm 2.4.5 → 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 (272) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
  4. package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
  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-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
  35. package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
  37. package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
  38. package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
  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-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
  44. package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  49. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  50. package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
  51. package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
  52. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  53. package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
  54. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  55. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  56. package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
  57. package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
  58. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  59. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  60. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  61. package/dist/cli-pkaqk57w.js +128 -0
  62. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  63. package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
  64. package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
  65. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  66. package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
  67. package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
  68. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  69. package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
  70. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  71. package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
  72. package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
  73. package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
  74. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  75. package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
  76. package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
  77. package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
  78. package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
  79. package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
  80. package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
  81. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  82. package/dist/cli.js +29 -27
  83. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  84. package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
  85. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  86. package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
  87. package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
  88. package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
  89. package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
  90. package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
  91. package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
  92. package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
  93. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  94. package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
  95. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  96. package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
  97. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  98. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  99. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  100. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  101. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  102. package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
  103. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  104. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  105. package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
  106. package/dist/handlers-c9ajzhbn.js +41 -0
  107. package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
  108. package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
  109. package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
  110. package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
  111. package/dist/index-07ef7nkm.js +42 -0
  112. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  113. package/dist/index-7yts8n9s.js +43 -0
  114. package/dist/index-q3t40ryv.js +46 -0
  115. package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
  116. package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
  117. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  118. package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
  119. package/dist/lead-aayq64jn.js +79 -0
  120. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  121. package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
  122. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  123. package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  125. package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
  126. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  127. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  128. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  129. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  130. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  131. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  132. package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
  133. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  134. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  135. package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
  136. package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
  137. package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
  138. package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
  139. package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
  140. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  141. package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
  142. package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
  143. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  144. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  145. package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
  146. package/dist/worker-nwb789mg.js +79 -0
  147. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  148. package/openapi.json +785 -5
  149. package/package.json +9 -9
  150. package/src/assets/key.ts +17 -0
  151. package/src/azure-devops/handlers.ts +1 -1
  152. package/src/be/db/approvals.ts +18 -0
  153. package/src/be/db/session-moves.ts +164 -0
  154. package/src/be/db/session-tree.ts +384 -0
  155. package/src/be/db/tasks/read.ts +40 -4
  156. package/src/be/db/workflow-runs.ts +96 -0
  157. package/src/be/db.ts +80 -236
  158. package/src/be/onboarding.ts +3 -0
  159. package/src/be/skill-sync.ts +43 -2
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/bitbucket/api.ts +81 -0
  162. package/src/bitbucket/auth.ts +64 -0
  163. package/src/bitbucket/handlers.ts +434 -0
  164. package/src/bitbucket/index.ts +28 -0
  165. package/src/bitbucket/templates.ts +134 -0
  166. package/src/bitbucket/types.ts +65 -0
  167. package/src/commands/runner.ts +55 -3
  168. package/src/e2b/env.ts +1 -0
  169. package/src/extensions/contract-types.generated.ts +1 -1
  170. package/src/extensions/contract.ts +2 -2
  171. package/src/extensions/post-bridge.ts +1 -0
  172. package/src/github/handlers.ts +6 -6
  173. package/src/gitlab/handlers.ts +5 -5
  174. package/src/http/assets.ts +14 -2
  175. package/src/http/core.ts +12 -0
  176. package/src/http/index.ts +4 -0
  177. package/src/http/onboarding.ts +1 -0
  178. package/src/http/sessions.ts +366 -12
  179. package/src/http/skills.ts +48 -1
  180. package/src/http/tasks.ts +1 -1
  181. package/src/http/users.ts +1 -0
  182. package/src/http/webhooks.ts +141 -0
  183. package/src/integrations/chat-sdk/adapters.ts +19 -3
  184. package/src/integrations/chat-sdk/facts.ts +30 -12
  185. package/src/integrations/chat-sdk/outbound.ts +38 -1
  186. package/src/providers/acp-targets.ts +1 -1
  187. package/src/providers/amp-adapter.ts +1 -1
  188. package/src/providers/pi-mono-adapter.ts +5 -53
  189. package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
  190. package/src/slack/handlers.test.ts +158 -0
  191. package/src/slack/handlers.ts +19 -28
  192. package/src/tasks/context-key.ts +61 -1
  193. package/src/tests/bitbucket-auth.test.ts +156 -0
  194. package/src/tests/bitbucket-handlers.test.ts +425 -0
  195. package/src/tests/bitbucket-webhook.test.ts +220 -0
  196. package/src/tests/chat-sdk-inbound.test.ts +39 -0
  197. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  198. package/src/tests/context-key.test.ts +50 -0
  199. package/src/tests/core-auth.test.ts +5 -0
  200. package/src/tests/db-task-read-contract.test.ts +7 -0
  201. package/src/tests/db-task-write-contract.test.ts +6 -0
  202. package/src/tests/docs-index-footprint.test.js +3 -3
  203. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  204. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  205. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  206. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  207. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  208. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  209. package/src/tests/generate-mcp-docs.test.ts +101 -0
  210. package/src/tests/gitlab-handlers.test.ts +3 -3
  211. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  212. package/src/tests/integration-disable-flags.test.ts +36 -0
  213. package/src/tests/mcp-tools.test.ts +1 -1
  214. package/src/tests/onboarding.test.ts +12 -0
  215. package/src/tests/pi-codemode.test.ts +8 -2
  216. package/src/tests/preload-env.test.ts +57 -0
  217. package/src/tests/preload.ts +21 -0
  218. package/src/tests/prompt-template-remaining.test.ts +38 -0
  219. package/src/tests/run-root-coverage.test.ts +39 -0
  220. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  221. package/src/tests/runner-skills-refresh.test.ts +58 -0
  222. package/src/tests/secret-scrubber.test.ts +15 -0
  223. package/src/tests/sessions-move.test.ts +576 -0
  224. package/src/tests/sessions-tree.test.ts +493 -0
  225. package/src/tests/skill-files-http.test.ts +50 -0
  226. package/src/tests/skills-bundle.test.ts +99 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +2 -1
  228. package/src/tests/vcs-bitbucket.test.ts +91 -0
  229. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  230. package/src/tests/vcs-provider.test.ts +16 -1
  231. package/src/tests/vcs-tracking.test.ts +3 -3
  232. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  233. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  234. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  235. package/src/tests/workflow-foreach.test.ts +44 -23
  236. package/src/tests/workflow-step-claims.test.ts +6 -6
  237. package/src/tests/workflow-tla-races.test.ts +2 -2
  238. package/src/tools/script-delete.ts +1 -1
  239. package/src/tools/workflows/create-workflow.ts +5 -3
  240. package/src/types.ts +3 -2
  241. package/src/utils/config-env-export.ts +1 -0
  242. package/src/utils/secret-scrubber.ts +4 -0
  243. package/src/utils/skills-refresh.ts +83 -43
  244. package/src/vcs/bitbucket.ts +58 -0
  245. package/src/vcs/index.ts +2 -0
  246. package/src/vcs/types.ts +2 -2
  247. package/src/workflows/approval-resolution.ts +4 -1
  248. package/src/workflows/checkpoint.ts +9 -1
  249. package/src/workflows/constants.ts +3 -0
  250. package/src/workflows/definition.ts +48 -17
  251. package/src/workflows/engine.ts +25 -15
  252. package/src/workflows/executors/foreach.ts +45 -99
  253. package/src/workflows/foreach-body.ts +146 -0
  254. package/src/workflows/foreach-driver.ts +552 -0
  255. package/src/workflows/foreach-join.ts +206 -7
  256. package/src/workflows/node-timeout.ts +21 -0
  257. package/src/workflows/recovery.ts +68 -3
  258. package/src/workflows/resume.ts +221 -43
  259. package/src/workflows/retry-poller.ts +39 -24
  260. package/src/workflows/step-input.ts +59 -0
  261. package/src/workflows/task-step-routing.ts +114 -56
  262. package/templates/skills/slack-interaction/content.md +5 -0
  263. package/templates/skills/work-on-task/SKILL.md +1 -1
  264. package/templates/skills/work-on-task/content.md +1 -1
  265. package/dist/cli-9hstj09h.js +0 -2657
  266. package/dist/cli-yw91rnmw.js +0 -13
  267. package/dist/handlers-7gw64dxw.js +0 -39
  268. package/dist/index-bgdyvcnv.js +0 -40
  269. package/dist/index-gabbcdn2.js +0 -43
  270. package/dist/index-tfb26zhy.js +0 -41
  271. package/dist/lead-7e02z839.js +0 -77
  272. package/dist/worker-5snytpbd.js +0 -77
@@ -1,8 +1,24 @@
1
- import { getLatestStepForNode, getWorkflowRunStepsByRunId } from "../be/db";
2
- import type { WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
1
+ import {
2
+ getDbClient,
3
+ getLatestStepForNode,
4
+ getWorkflowRun,
5
+ getWorkflowRunStep,
6
+ getWorkflowRunStepsByRunId,
7
+ updateWorkflowRun,
8
+ updateWorkflowRunStep,
9
+ } from "../be/db";
10
+ import type {
11
+ WorkflowDefinition,
12
+ WorkflowNode,
13
+ WorkflowRunStep,
14
+ WorkflowRunStepStatus,
15
+ } from "../types";
3
16
  import { checkpointStep } from "./checkpoint";
4
17
  import { getSuccessors } from "./definition";
5
18
  import type { ForeachOutput } from "./executors/foreach";
19
+ import type { ExecutorRegistry } from "./executors/registry";
20
+ import { effectiveFailurePolicy } from "./foreach-body";
21
+ import { startForeachDrive } from "./foreach-driver";
6
22
 
7
23
  export const FOREACH_TERMINAL_STEP_STATUSES = new Set([
8
24
  "completed",
@@ -29,7 +45,10 @@ export function parseSyntheticNodeId(nodeId: string): SyntheticNodeId | null {
29
45
  };
30
46
  }
31
47
 
32
- export function resolveForeachParent(def: WorkflowDefinition, nodeId: string): WorkflowNode | null {
48
+ export function resolveForeachParent(
49
+ def: Pick<WorkflowDefinition, "nodes">,
50
+ nodeId: string,
51
+ ): WorkflowNode | null {
33
52
  const parsed = parseSyntheticNodeId(nodeId);
34
53
  if (!parsed) return null;
35
54
  const parent = def.nodes.find((node) => node.id === parsed.parentNodeId);
@@ -53,10 +72,9 @@ export async function joinForeach(
53
72
  throw new Error(`Step "${childStep.nodeId}" is not a foreach child`);
54
73
  }
55
74
 
56
- const children = (await getWorkflowRunStepsByRunId(runId))
57
- .filter((step) => resolveForeachParent(def, step.nodeId)?.id === parent.id)
58
- .sort((a, b) => childIndex(a) - childIndex(b));
75
+ const children = latestForeachChildren(def, parent.id, await getWorkflowRunStepsByRunId(runId));
59
76
 
77
+ // `pending` (queued, not started) is non-terminal like running and waiting.
60
78
  if (children.some((step) => !FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
61
79
  return { joined: false, parentNodeId: parent.id, successors: [] };
62
80
  }
@@ -81,8 +99,28 @@ export async function joinForeach(
81
99
  };
82
100
  }
83
101
 
102
+ /**
103
+ * The latest step row per child node id of foreach `parentNodeId`, in item
104
+ * order. `steps` must be in insert order (as `getWorkflowRunStepsByRunId`
105
+ * returns them), so a later attempt row supersedes an earlier one.
106
+ */
107
+ export function latestForeachChildren(
108
+ def: WorkflowDefinition,
109
+ parentNodeId: string,
110
+ steps: WorkflowRunStep[],
111
+ ): WorkflowRunStep[] {
112
+ const latest = new Map<string, WorkflowRunStep>();
113
+ for (const step of steps) {
114
+ if (resolveForeachParent(def, step.nodeId)?.id === parentNodeId) latest.set(step.nodeId, step);
115
+ }
116
+ return [...latest.values()].sort((a, b) => childIndex(a) - childIndex(b));
117
+ }
118
+
84
119
  export function buildForeachAggregate(children: WorkflowRunStep[]): ForeachOutput {
85
- const orderedChildren = [...children].sort((a, b) => childIndex(a) - childIndex(b));
120
+ // Only the latest row per child node id counts (insert order).
121
+ const latest = new Map<string, WorkflowRunStep>();
122
+ for (const step of children) latest.set(step.nodeId, step);
123
+ const orderedChildren = [...latest.values()].sort((a, b) => childIndex(a) - childIndex(b));
86
124
  const results: ForeachOutput["results"] = orderedChildren.map((step) => {
87
125
  const parsed = parseSyntheticNodeId(step.nodeId)!;
88
126
  const status = resultStatus(step);
@@ -92,11 +130,15 @@ export function buildForeachAggregate(children: WorkflowRunStep[]): ForeachOutpu
92
130
  output: step.output,
93
131
  };
94
132
  });
133
+ // Only `completed` without an error is ok: failed, cancelled, skipped and the
134
+ // legacy completed+error shape all count as failed.
95
135
  const failedCount = results.filter((result) => result.status !== "completed").length;
136
+ const cancelledCount = results.filter((result) => result.status === "cancelled").length;
96
137
  const aggregate: ForeachOutput = {
97
138
  results,
98
139
  okCount: results.length - failedCount,
99
140
  failedCount,
141
+ ...(cancelledCount > 0 ? { cancelledCount } : {}),
100
142
  };
101
143
  return aggregate;
102
144
  }
@@ -115,3 +157,160 @@ function resultStatus(step: WorkflowRunStep): ForeachOutput["results"][number]["
115
157
  // (e.g. an agent quoting a log line) is user-controlled text, not a failure.
116
158
  return step.error ? "failed" : "completed";
117
159
  }
160
+
161
+ /** The terminal outcome of one foreach item. */
162
+ export interface ForeachChildOutcome {
163
+ status: "completed" | "failed" | "cancelled";
164
+ output?: unknown;
165
+ /**
166
+ * The failure reason. On a `completed` outcome it marks the legacy
167
+ * `onNodeFailure: "continue"` shape (completed with error), which the
168
+ * aggregate counts as failed.
169
+ */
170
+ error?: string;
171
+ /** Stored on the child row for display only: a body's ports do not route. */
172
+ nextPort?: string;
173
+ }
174
+
175
+ export interface ForeachSettleResult {
176
+ /** False when the child was not in an expected status, or the run is no longer live. */
177
+ claimed: boolean;
178
+ foreachChild: true;
179
+ /** True when this settlement closed the join and checkpointed the parent. */
180
+ joined: boolean;
181
+ /** The parent's successors to walk, when `joined`. */
182
+ successors: WorkflowNode[];
183
+ /** True when this settlement failed the run (a failed item under fail-fast). */
184
+ runFailed: boolean;
185
+ }
186
+
187
+ /**
188
+ * The one place a foreach child reaches a terminal status. Every resume path
189
+ * (task, approval, wait, child run, and their failure paths) and the foreach
190
+ * driver's inline items settle through here.
191
+ *
192
+ * In one transaction:
193
+ * 1. Re-read the child; claim it only while it is in one of `from` (default
194
+ * `waiting`, the parked status every resume path expects).
195
+ * 2. Write the terminal status, output, error and port.
196
+ * 3. A run that is no longer live (failed, cancelled) only records the
197
+ * outcome: a late sibling must not revive a failed run or route its join.
198
+ * 4. A failed or cancelled item under fail-fast (see effectiveFailurePolicy)
199
+ * cancels the queued siblings and fails the run; in-flight siblings still
200
+ * record their outcomes. Under collect-outcomes it counts as failed in the
201
+ * aggregate and the join proceeds.
202
+ * 5. Otherwise run the join and set the run `running`.
203
+ *
204
+ * When the join stays open with a queued (`pending`) item and a `registry` is
205
+ * passed, a drive of the parent starts after commit, so the next item starts.
206
+ * Orphaned `running` items are the recovery sweep's.
207
+ */
208
+ export async function settleForeachChild(
209
+ def: WorkflowDefinition,
210
+ runId: string,
211
+ childStepId: string,
212
+ outcome: ForeachChildOutcome,
213
+ ctx: Record<string, unknown>,
214
+ options: {
215
+ from?: readonly WorkflowRunStepStatus[];
216
+ registry?: ExecutorRegistry;
217
+ /** Run error when this outcome fails the run. Defaults to the outcome error. */
218
+ runError?: string;
219
+ } = {},
220
+ ): Promise<ForeachSettleResult> {
221
+ const from = options.from ?? ["waiting"];
222
+ return await getDbClient().transaction(async (): Promise<ForeachSettleResult> => {
223
+ const unclaimed: ForeachSettleResult = {
224
+ claimed: false,
225
+ foreachChild: true,
226
+ joined: false,
227
+ successors: [],
228
+ runFailed: false,
229
+ };
230
+ const current = await getWorkflowRunStep(childStepId);
231
+ if (!current || !from.includes(current.status)) return unclaimed;
232
+ const parent = resolveForeachParent(def, current.nodeId);
233
+ if (!parent) throw new Error(`Step "${current.nodeId}" is not a foreach child`);
234
+ const run = await getWorkflowRun(runId);
235
+ if (!run) return unclaimed;
236
+
237
+ const finishedAt = new Date().toISOString();
238
+ await updateWorkflowRunStep(childStepId, {
239
+ status: outcome.status,
240
+ ...(outcome.output !== undefined ? { output: outcome.output } : {}),
241
+ ...(outcome.error !== undefined ? { error: outcome.error } : {}),
242
+ ...(outcome.nextPort ? { nextPort: outcome.nextPort } : {}),
243
+ finishedAt,
244
+ });
245
+
246
+ if (run.status !== "running" && run.status !== "waiting") return unclaimed;
247
+
248
+ if (outcome.status !== "completed" && effectiveFailurePolicy(parent, def) === "fail-fast") {
249
+ const reason = options.runError ?? outcome.error ?? `foreach item ${current.nodeId} failed`;
250
+ await cancelQueuedForeachChildren(def, runId, parent.id, current.nodeId);
251
+ await updateWorkflowRun(runId, { status: "failed", error: reason, finishedAt });
252
+ return { ...unclaimed, claimed: true, runFailed: true };
253
+ }
254
+
255
+ const join = await joinForeach(def, runId, current, ctx);
256
+ await updateWorkflowRun(runId, { status: "running" });
257
+ const registry = options.registry;
258
+ if (!join.joined && registry && (await hasQueuedChild(def, runId, parent.id))) {
259
+ getDbClient().afterCommit(() => startForeachDrive(runId, parent.id, registry));
260
+ }
261
+ return {
262
+ claimed: true,
263
+ foreachChild: true,
264
+ joined: join.joined,
265
+ successors: join.successors,
266
+ runFailed: false,
267
+ };
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Fail-fast: cancel every queued (`pending`) child of the fan-out so none of
273
+ * them starts. In-flight children are left to finish and record their outcome.
274
+ * A run retry gives each cancelled item a new attempt.
275
+ */
276
+ export async function cancelQueuedForeachChildren(
277
+ def: WorkflowDefinition,
278
+ runId: string,
279
+ parentNodeId: string,
280
+ failedChildNodeId?: string,
281
+ ): Promise<number> {
282
+ const failedKey = failedChildNodeId
283
+ ? (parseSyntheticNodeId(failedChildNodeId)?.itemKey ?? failedChildNodeId)
284
+ : undefined;
285
+ const error = failedKey
286
+ ? `not started: fail-fast after item ${failedKey} failed`
287
+ : "not started: fail-fast after the run failed";
288
+ const finishedAt = new Date().toISOString();
289
+ let cancelled = 0;
290
+ await getDbClient().transaction(async () => {
291
+ const children = latestForeachChildren(
292
+ def,
293
+ parentNodeId,
294
+ await getWorkflowRunStepsByRunId(runId),
295
+ );
296
+ for (const child of children) {
297
+ if (child.status !== "pending") continue;
298
+ const row = await getDbClient().get<{ id: string }>(
299
+ "UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ? WHERE id = ? AND status = 'pending' RETURNING id",
300
+ [error, finishedAt, child.id],
301
+ );
302
+ if (row) cancelled++;
303
+ }
304
+ });
305
+ return cancelled;
306
+ }
307
+
308
+ async function hasQueuedChild(
309
+ def: WorkflowDefinition,
310
+ runId: string,
311
+ parentNodeId: string,
312
+ ): Promise<boolean> {
313
+ return latestForeachChildren(def, parentNodeId, await getWorkflowRunStepsByRunId(runId)).some(
314
+ (child) => child.status === "pending",
315
+ );
316
+ }
@@ -0,0 +1,21 @@
1
+ import type { WorkflowNode } from "../types";
2
+
3
+ /** Watchdog budget for a node that configures none. */
4
+ export const DEFAULT_NODE_TIMEOUT_MS = 30_000;
5
+
6
+ /**
7
+ * The workflow watchdog budget for one executor call of `node`.
8
+ *
9
+ * Inline script nodes historically expose their wall-clock budget as
10
+ * `config.timeout`, while the workflow watchdog reads `config.timeoutMs` for
11
+ * every other executor. Keep the outer watchdog aligned with the script
12
+ * executor so a script configured for (for example) 90s is not killed by the
13
+ * 30s default first. The foreach driver applies this to each child with the
14
+ * synthesized body node, so every item gets its own budget.
15
+ */
16
+ export function resolveNodeTimeoutMs(node: Pick<WorkflowNode, "type" | "config">): number {
17
+ const config = node.config ?? {};
18
+ if (typeof config.timeoutMs === "number") return config.timeoutMs;
19
+ if (node.type === "script" && typeof config.timeout === "number") return config.timeout;
20
+ return DEFAULT_NODE_TIMEOUT_MS;
21
+ }
@@ -24,6 +24,8 @@ import {
24
24
  walkGraph,
25
25
  } from "./engine";
26
26
  import type { ExecutorRegistry } from "./executors/registry";
27
+ import { stepFailureMode } from "./foreach-body";
28
+ import { isForeachChildOwned, runForeachDrive } from "./foreach-driver";
27
29
  import { getSecretInputKeys } from "./input";
28
30
  import {
29
31
  finalizeOrWait,
@@ -71,6 +73,9 @@ export async function recoverIncompleteRuns(registry: ExecutorRegistry): Promise
71
73
  }
72
74
  }
73
75
 
76
+ // --- Case 6: foreach fan-outs with queued or orphaned items ---
77
+ recovered += await recoverForeachFanouts(registry);
78
+
74
79
  if (recovered > 0) {
75
80
  console.log(`[workflows] Recovered ${recovered} incomplete run(s)`);
76
81
  }
@@ -211,20 +216,22 @@ async function recoverWaitingRuns(registry: ExecutorRegistry): Promise<number> {
211
216
  if (retry === "scheduled") recovered++;
212
217
  if (retry !== "not-eligible") continue;
213
218
  }
214
- if (!taskCompleted && (workflow.definition.onNodeFailure ?? "fail") === "fail") {
219
+ if (!taskCompleted && stepFailureMode(workflow.definition, stuck.nodeId) === "fail") {
215
220
  // Preserve the fail-fast recovery policy for failed/cancelled tasks.
216
221
  // Claimed: this sweep runs on every heartbeat, so the live task.failed
217
222
  // bus event may already have routed this step — a blind write here
218
223
  // would kill a run that is already advancing.
219
224
  const reason =
220
225
  stuck.taskStatus === "failed" ? "Task failed (recovered)" : "Task cancelled (recovered)";
221
- const claimed = await failStepAndRunIfWaiting(
226
+ const failed = await failStepAndRunIfWaiting(
227
+ workflow.definition,
222
228
  stuck.stepId,
223
229
  stuck.runId,
224
230
  reason,
225
231
  stuck.taskId,
232
+ { registry },
226
233
  );
227
- if (!claimed) continue;
234
+ if (!failed.claimed) continue;
228
235
  recovered++;
229
236
  continue;
230
237
  }
@@ -248,6 +255,7 @@ async function recoverWaitingRuns(registry: ExecutorRegistry): Promise<number> {
248
255
  ctx,
249
256
  taskCompleted ? undefined : reason,
250
257
  stuck.taskId,
258
+ registry,
251
259
  );
252
260
  if (!routing.claimed) continue;
253
261
  if (routing.foreachChild && !routing.joined) {
@@ -351,6 +359,7 @@ async function recoverApprovalWaitingRuns(registry: ExecutorRegistry): Promise<n
351
359
  stepOutput,
352
360
  nextPort,
353
361
  ctx,
362
+ registry,
354
363
  );
355
364
  });
356
365
  if (!routing?.claimed) continue;
@@ -413,6 +422,62 @@ async function recoverWaitStates(registry: ExecutorRegistry): Promise<number> {
413
422
  return recovered;
414
423
  }
415
424
 
425
+ /**
426
+ * Re-drive foreach fan-outs of live runs whose parent is still open and that
427
+ * have a queued (`pending`) item, or a `running` item no drive in this process
428
+ * owns. Catches a crash after a settlement committed and before the next
429
+ * drive, and items a crash left between their claim and their settlement. An
430
+ * orphaned instant item runs again (at-least-once, like a top-level instant
431
+ * node); an async item reconnects, because its executor dedupes on the child
432
+ * step id.
433
+ */
434
+ async function recoverForeachFanouts(registry: ExecutorRegistry): Promise<number> {
435
+ const rows = await getDbClient().query<{
436
+ runId: string;
437
+ workflowId: string;
438
+ stepId: string;
439
+ nodeId: string;
440
+ status: string;
441
+ }>(
442
+ `SELECT s.runId AS runId, r.workflowId AS workflowId, s.id AS stepId, s.nodeId AS nodeId, s.status AS status
443
+ FROM workflow_run_steps s
444
+ JOIN workflow_runs r ON r.id = s.runId
445
+ WHERE r.status IN ('running', 'waiting')
446
+ AND s.status IN ('pending', 'running')
447
+ AND instr(s.nodeId, '#') > 1`,
448
+ );
449
+ const fanouts = new Map<string, { runId: string; workflowId: string; parentNodeId: string }>();
450
+ for (const row of rows) {
451
+ if (row.status === "running" && isForeachChildOwned(row.stepId)) continue;
452
+ const parentNodeId = row.nodeId.slice(0, row.nodeId.indexOf("#"));
453
+ fanouts.set(`${row.runId}\u0000${parentNodeId}`, {
454
+ runId: row.runId,
455
+ workflowId: row.workflowId,
456
+ parentNodeId,
457
+ });
458
+ }
459
+
460
+ let recovered = 0;
461
+ for (const fanout of fanouts.values()) {
462
+ try {
463
+ // The drive re-checks the run, the parent step and each child's latest
464
+ // row, so a stale snapshot row claims nothing.
465
+ const workflow = await getWorkflow(fanout.workflowId);
466
+ const parent = workflow?.definition.nodes.find((node) => node.id === fanout.parentNodeId);
467
+ if (parent?.type !== "foreach") continue;
468
+ const drive = await runForeachDrive(fanout.runId, fanout.parentNodeId, registry);
469
+ if (drive.claimed > 0) recovered++;
470
+ } catch (err) {
471
+ if (await failRunOnUnreadableReplay(fanout.runId, err)) continue;
472
+ console.error(
473
+ `[workflows] Failed to re-drive foreach ${fanout.parentNodeId} of run ${fanout.runId}:`,
474
+ err,
475
+ );
476
+ }
477
+ }
478
+ return recovered;
479
+ }
480
+
416
481
  /**
417
482
  * Get run IDs by status. Simple query since there's no dedicated function for this.
418
483
  */