@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
@@ -17,8 +17,14 @@ import {
17
17
  updateWorkflowRun,
18
18
  updateWorkflowRunStep,
19
19
  } from "../be/db";
20
- import { getChildWorkflowRunId } from "../be/db/workflow-runs";
21
- import type { WaitStateRow } from "../types";
20
+ import { createForeachAttemptSteps, getChildWorkflowRunId } from "../be/db/workflow-runs";
21
+ import type {
22
+ WaitStateRow,
23
+ WorkflowDefinition,
24
+ WorkflowNode,
25
+ WorkflowRun,
26
+ WorkflowRunStep,
27
+ } from "../types";
22
28
  import { scrubSecrets } from "../utils/secret-scrubber";
23
29
  import {
24
30
  type ApprovalSlackClient,
@@ -34,8 +40,17 @@ import { workflowEventBus } from "./event-bus";
34
40
  import type { ExecutorRegistry } from "./executors/registry";
35
41
  import { childRunOutcome } from "./executors/sub-workflow";
36
42
  import { computeNextPort } from "./executors/wait";
37
- import { resolveForeachParent } from "./foreach-join";
43
+ import { stepFailureMode } from "./foreach-body";
44
+ import { startForeachDrive } from "./foreach-driver";
45
+ import {
46
+ FOREACH_TERMINAL_STEP_STATUSES,
47
+ type ForeachChildOutcome,
48
+ parseSyntheticNodeId,
49
+ resolveForeachParent,
50
+ settleForeachChild,
51
+ } from "./foreach-join";
38
52
  import { getSecretInputKeys } from "./input";
53
+ import { getMaxWorkflowStepsPerRun } from "./limits";
39
54
  import { findWorkflowReadinessProblems, formatReadinessProblems } from "./readiness";
40
55
  import {
41
56
  checkpointPortStepAndResolveSuccessors,
@@ -181,17 +196,7 @@ async function resumeFromTaskCompletionUnguarded(
181
196
  registry: ExecutorRegistry,
182
197
  ): Promise<void> {
183
198
  const run = await getWorkflowRun(event.workflowRunId!);
184
- if (!run || (run.status !== "waiting" && run.status !== "running")) return;
185
-
186
- const step = await getWorkflowRunStep(event.workflowRunStepId!);
187
- if (!step || step.status !== "waiting") return;
188
- if (await isStaleTaskEvent(step.id, event)) return;
189
-
190
- const workflow = await getWorkflow(run.workflowId);
191
- if (!workflow) return;
192
-
193
- // Checkpoint: atomic step completion + context update
194
- const ctx = (run.context ?? {}) as Record<string, unknown>;
199
+ if (!run) return;
195
200
 
196
201
  // JSON-parse structured output so downstream nodes can access nested fields
197
202
  let taskOutput: unknown = event.output;
@@ -207,12 +212,33 @@ async function resumeFromTaskCompletionUnguarded(
207
212
  }
208
213
  const stepOutput = { taskId: event.taskId, taskOutput };
209
214
 
215
+ if (run.status !== "waiting" && run.status !== "running") {
216
+ await recordLateForeachOutcome(run, event.workflowRunStepId!, event, {
217
+ status: "completed",
218
+ output: stepOutput,
219
+ });
220
+ return;
221
+ }
222
+
223
+ const step = await getWorkflowRunStep(event.workflowRunStepId!);
224
+ if (!step || step.status !== "waiting") return;
225
+ if (await isStaleTaskEvent(step.id, event)) return;
226
+
227
+ const workflow = await getWorkflow(run.workflowId);
228
+ if (!workflow) return;
229
+
230
+ // Checkpoint: atomic step completion + context update
231
+ const ctx = (run.context ?? {}) as Record<string, unknown>;
232
+
210
233
  const routing = await completeTaskStepAndResolveSuccessors(
211
234
  workflow.definition,
212
235
  run.id,
213
236
  step,
214
237
  stepOutput,
215
238
  ctx,
239
+ undefined,
240
+ undefined,
241
+ registry,
216
242
  );
217
243
  // Another handler already routed this step — routing it twice would create
218
244
  // duplicate successor steps.
@@ -299,7 +325,14 @@ async function handleTaskFailureUnguarded(
299
325
  options: { retryable: boolean },
300
326
  ): Promise<void> {
301
327
  const run = await getWorkflowRun(event.workflowRunId!);
302
- if (!run || (run.status !== "waiting" && run.status !== "running")) return;
328
+ if (!run) return;
329
+ if (run.status !== "waiting" && run.status !== "running") {
330
+ await recordLateForeachOutcome(run, event.workflowRunStepId!, event, {
331
+ status: "failed",
332
+ error: reason,
333
+ });
334
+ return;
335
+ }
303
336
 
304
337
  const step = await getWorkflowRunStep(event.workflowRunStepId!);
305
338
  if (!step || step.status !== "waiting") return;
@@ -325,10 +358,12 @@ async function handleTaskFailureUnguarded(
325
358
  if (retry === "not-claimed") return;
326
359
  }
327
360
 
328
- const onFailure = workflow.definition.onNodeFailure ?? "fail";
361
+ // A foreach item follows its fan-out's failure policy (fail-fast or
362
+ // collect-outcomes); any other step follows the workflow's onNodeFailure.
363
+ const onFailure = stepFailureMode(workflow.definition, step.nodeId);
329
364
 
330
365
  if (onFailure === "fail") {
331
- await markRunFailed(event, reason);
366
+ await markRunFailed(workflow.definition, event, reason, registry);
332
367
  return;
333
368
  }
334
369
 
@@ -345,6 +380,8 @@ async function handleTaskFailureUnguarded(
345
380
  stepOutput,
346
381
  ctx,
347
382
  reason,
383
+ undefined,
384
+ registry,
348
385
  );
349
386
  if (!routing.claimed) return;
350
387
 
@@ -367,6 +404,32 @@ async function handleTaskFailureUnguarded(
367
404
  }
368
405
  }
369
406
 
407
+ /**
408
+ * A fail-fast fan-out lets its in-flight items finish after the run failed.
409
+ * Record such a late outcome on the item row so the run's history (and a
410
+ * later retry) sees it; the run itself stays failed and nothing routes.
411
+ */
412
+ async function recordLateForeachOutcome(
413
+ run: WorkflowRun,
414
+ stepId: string,
415
+ event: TaskEvent,
416
+ outcome: ForeachChildOutcome,
417
+ ): Promise<void> {
418
+ if (run.status !== "failed") return;
419
+ const step = await getWorkflowRunStep(stepId);
420
+ if (!step || step.status !== "waiting") return;
421
+ if (await isStaleTaskEvent(step.id, event)) return;
422
+ const workflow = await getWorkflow(run.workflowId);
423
+ if (!workflow || !resolveForeachParent(workflow.definition, step.nodeId)) return;
424
+ await settleForeachChild(
425
+ workflow.definition,
426
+ run.id,
427
+ step.id,
428
+ outcome,
429
+ (run.context ?? {}) as Record<string, unknown>,
430
+ );
431
+ }
432
+
370
433
  /**
371
434
  * A retried step has a NEW task bound to it. Task lifecycle events are emitted
372
435
  * from several places (the db mutators' after-commit emits, test/manual emits,
@@ -386,8 +449,20 @@ async function isStaleTaskEvent(stepId: string, event: TaskEvent): Promise<boole
386
449
  * both route here and can also race a recovery sweep or a completion that
387
450
  * already claimed the step — only the first writer may fail the run.
388
451
  */
389
- async function markRunFailed(event: TaskEvent, reason: string): Promise<void> {
390
- await failStepAndRunIfWaiting(event.workflowRunStepId!, event.workflowRunId!, reason);
452
+ async function markRunFailed(
453
+ def: WorkflowDefinition,
454
+ event: TaskEvent,
455
+ reason: string,
456
+ registry: ExecutorRegistry,
457
+ ): Promise<void> {
458
+ await failStepAndRunIfWaiting(
459
+ def,
460
+ event.workflowRunStepId!,
461
+ event.workflowRunId!,
462
+ reason,
463
+ undefined,
464
+ { registry },
465
+ );
391
466
  }
392
467
 
393
468
  /**
@@ -400,9 +475,11 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
400
475
  const workflow = await getWorkflow(run.workflowId);
401
476
  if (!workflow) throw new Error("Workflow not found");
402
477
 
403
- // Find the failed step
478
+ // Find the failed step. Only the latest row per node id counts: a foreach
479
+ // item keeps its earlier attempt rows as history.
404
480
  const steps = await getWorkflowRunStepsByRunId(runId);
405
- const failedStep = steps.find((s) => s.status === "failed");
481
+ const latestSteps = [...latestStepPerNode(steps).values()];
482
+ const failedStep = latestSteps.find((s) => s.status === "failed");
406
483
  if (!failedStep) throw new Error("No failed step found");
407
484
 
408
485
  const completedNodeIds = new Set(await getCompletedStepNodeIds(runId));
@@ -438,17 +515,36 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
438
515
  // (HTTP route + MCP tool) both pass the guard above, and a double reset
439
516
  // walks the same failed node twice.
440
517
  const ctx = (run.context ?? {}) as Record<string, unknown>;
441
- const claimed = await getDbClient().transaction(async () => {
518
+ const attempts = await getDbClient().transaction(async () => {
442
519
  const current = await getWorkflowRun(runId);
443
- if (!current || current.status !== "failed") return false;
444
- await updateWorkflowRunStep(failedStep.id, {
445
- status: shouldRetryFailedNode ? "pending" : "cancelled",
446
- error: shouldRetryFailedNode ? null : "Skipped on retry because its branch was not active",
447
- });
520
+ if (!current || current.status !== "failed") return null;
521
+ // Fan-outs with failed or cancelled items get one new attempt row per such
522
+ // item; completed items are never rerun. Selected from the rows as of this
523
+ // transaction: an in-flight sibling of a fail-fast fan-out can record its
524
+ // late failure while the readiness check above is awaited.
525
+ const currentSteps = await getWorkflowRunStepsByRunId(runId);
526
+ const attempts = foreachRetryAttempts(workflow.definition, [
527
+ ...latestStepPerNode(currentSteps).values(),
528
+ ]);
529
+ const capError = foreachRetryCapError(currentSteps.length, attempts);
530
+ if (capError) throw new Error(`Retry not started, run left failed: ${capError}`);
531
+ // A foreach item retries as a new attempt row instead of a reset: a fresh
532
+ // step id is a fresh invocation identity for every body type, and the
533
+ // failed row stays as history.
534
+ if (!foreachParent) {
535
+ await updateWorkflowRunStep(failedStep.id, {
536
+ status: shouldRetryFailedNode ? "pending" : "cancelled",
537
+ error: shouldRetryFailedNode ? null : "Skipped on retry because its branch was not active",
538
+ });
539
+ }
540
+ await createForeachAttemptSteps(
541
+ runId,
542
+ attempts.flatMap((attempt) => attempt.rows),
543
+ );
448
544
  await updateWorkflowRun(runId, { status: "running", error: null, context: ctx });
449
- return true;
545
+ return attempts;
450
546
  });
451
- if (!claimed) throw new Error("Run is not in failed state");
547
+ if (!attempts) throw new Error("Run is not in failed state");
452
548
 
453
549
  // Resume from the failed node — use findReadyNodes for convergence safety.
454
550
  // findReadyNodes returns every node without a completed step, including a
@@ -469,6 +565,75 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
469
565
  : [failedNode, ...readyNodes];
470
566
  const secretKeys = getSecretInputKeys(workflow.input);
471
567
  await walkGraph(workflow.definition, runId, ctx, nodesToRun, registry, workflow.id, secretKeys);
568
+ // Another fan-out with new attempts is not on the walk (its parent step is
569
+ // still waiting): drive it directly.
570
+ for (const attempt of attempts) {
571
+ if (attempt.parent.id !== failedNode.id) {
572
+ startForeachDrive(runId, attempt.parent.id, registry);
573
+ }
574
+ }
575
+ }
576
+
577
+ /** The latest row per node id, in insert order of each node's first row. */
578
+ function latestStepPerNode(steps: WorkflowRunStep[]): Map<string, WorkflowRunStep> {
579
+ const latest = new Map<string, WorkflowRunStep>();
580
+ for (const step of steps) latest.set(step.nodeId, step);
581
+ return latest;
582
+ }
583
+
584
+ interface ForeachRetryAttempts {
585
+ parent: WorkflowNode;
586
+ rows: Array<{ nodeId: string; nodeType: string; itemKey: string; index: number }>;
587
+ }
588
+
589
+ /**
590
+ * The new attempt rows a run retry adds: one per foreach item whose latest
591
+ * row is `failed` or `cancelled`, for every fan-out whose parent step is still
592
+ * open (a closed join already routed its successors).
593
+ */
594
+ function foreachRetryAttempts(
595
+ def: WorkflowDefinition,
596
+ latestSteps: WorkflowRunStep[],
597
+ ): ForeachRetryAttempts[] {
598
+ const byParent = new Map<string, ForeachRetryAttempts>();
599
+ const latestByNode = new Map(latestSteps.map((step) => [step.nodeId, step]));
600
+ for (const step of latestSteps) {
601
+ if (step.status !== "failed" && step.status !== "cancelled") continue;
602
+ const parent = resolveForeachParent(def, step.nodeId);
603
+ if (!parent) continue;
604
+ const parentStep = latestByNode.get(parent.id);
605
+ if (!parentStep || FOREACH_TERMINAL_STEP_STATUSES.has(parentStep.status)) continue;
606
+ const input = (step.input ?? {}) as Record<string, unknown>;
607
+ const entry = byParent.get(parent.id) ?? { parent, rows: [] };
608
+ entry.rows.push({
609
+ nodeId: step.nodeId,
610
+ nodeType: step.nodeType,
611
+ itemKey:
612
+ typeof input.itemKey === "string"
613
+ ? input.itemKey
614
+ : (parseSyntheticNodeId(step.nodeId)?.itemKey ?? ""),
615
+ index: typeof input.index === "number" ? input.index : 0,
616
+ });
617
+ byParent.set(parent.id, entry);
618
+ }
619
+ return [...byParent.values()];
620
+ }
621
+
622
+ /**
623
+ * Refuse a retry whose new attempt rows would breach WORKFLOW_MAX_STEPS_PER_RUN.
624
+ * The retry's own walk runs walkGraph's circuit breaker after the rows are
625
+ * inserted, and it trips at `>= max`, so the rows must leave one step of
626
+ * headroom even for a terminal fan-out.
627
+ */
628
+ function foreachRetryCapError(
629
+ existingSteps: number,
630
+ attempts: ForeachRetryAttempts[],
631
+ ): string | null {
632
+ const added = attempts.reduce((sum, attempt) => sum + attempt.rows.length, 0);
633
+ if (added === 0) return null;
634
+ const maxSteps = getMaxWorkflowStepsPerRun();
635
+ if (existingSteps + added < maxSteps) return null;
636
+ return `foreach retry would reach ${maxSteps} total steps (WORKFLOW_MAX_STEPS_PER_RUN): ${existingSteps} existing + ${added} new attempts leaves no room for the retry walk`;
472
637
  }
473
638
 
474
639
  /**
@@ -646,6 +811,7 @@ async function resumeFromApprovalResolutionUnguarded(
646
811
  stepOutput,
647
812
  nextPort,
648
813
  ctx,
814
+ registry,
649
815
  );
650
816
  // Another handler (the recovery sweep) already routed this step — routing
651
817
  // it twice would create duplicate successor steps.
@@ -768,6 +934,7 @@ async function resumeClaimedWait(
768
934
  stepOutput,
769
935
  nextPort,
770
936
  ctx,
937
+ registry,
771
938
  );
772
939
  if (!routing.claimed) return;
773
940
 
@@ -822,22 +989,33 @@ export async function resumeFromChildRun(
822
989
  const workflow = await getWorkflow(run.workflowId);
823
990
  if (!workflow) return;
824
991
 
825
- if ("error" in outcome) {
826
- claimed = await failStepAndRunIfWaiting(step.id, run.id, outcome.error);
992
+ const ctx = (run.context ?? {}) as Record<string, unknown>;
993
+ // A foreach item's failure settles the item: under `onNodeFailure:
994
+ // "continue"` it can close the join, so it routes like a success.
995
+ const routing =
996
+ "error" in outcome
997
+ ? await failStepAndRunIfWaiting(
998
+ workflow.definition,
999
+ step.id,
1000
+ run.id,
1001
+ outcome.error,
1002
+ undefined,
1003
+ { ctx, registry },
1004
+ )
1005
+ : await checkpointPortStepAndResolveSuccessors(
1006
+ workflow.definition,
1007
+ run.id,
1008
+ step.id,
1009
+ step.nodeId,
1010
+ outcome.output,
1011
+ "success",
1012
+ ctx,
1013
+ registry,
1014
+ );
1015
+ if (!routing.claimed || routing.runFailed) {
1016
+ claimed = routing.claimed;
827
1017
  return;
828
1018
  }
829
-
830
- const ctx = (run.context ?? {}) as Record<string, unknown>;
831
- const routing = await checkpointPortStepAndResolveSuccessors(
832
- workflow.definition,
833
- run.id,
834
- step.id,
835
- step.nodeId,
836
- outcome.output,
837
- "success",
838
- ctx,
839
- );
840
- if (!routing.claimed) return;
841
1019
  claimed = true;
842
1020
 
843
1021
  if (routing.successors.length > 0) {
@@ -10,16 +10,15 @@ import type { RetryPolicy } from "../types";
10
10
  import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
11
11
  import { getSuccessors, resolveValidationPort } from "./definition";
12
12
  import {
13
- buildNodeInterpolationCtx,
14
13
  failRunOnUnreadableReplay,
15
14
  holdWorkflowRun,
16
- interpolateNodeConfig,
17
- rehydrateCompletedStepOutputs,
18
15
  scriptBodyInterpolationError,
19
16
  walkGraph,
20
17
  } from "./engine";
21
18
  import type { AsyncExecutorResult } from "./executors/base";
22
19
  import type { ExecutorRegistry } from "./executors/registry";
20
+ import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach } from "./foreach-driver";
21
+ import { rebuildNodeExecutionInput } from "./step-input";
23
22
  import { runStepValidation } from "./validation";
24
23
 
25
24
  let pollerTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -63,27 +62,16 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
63
62
  // win, or the step re-executes inside a cancelled run.
64
63
  if (!(await claimRetry(run.id, step.id))) continue;
65
64
 
66
- const ctx = (run.context ?? {}) as Record<string, unknown>;
67
- // A step can fail before ANY checkpoint persisted the walkGraph-hydrated
68
- // context, so `run.id` may be absent here — mirror the hydration or the
69
- // builtin resolves to "" on every retry.
70
- if (!("run" in ctx)) ctx.run = { id: run.id };
71
-
72
- // The persisted run context can lag behind completed step checkpoints.
73
- // Reuse walkGraph's recovery path before resolving declared inputs so
74
- // retries see the same upstream outputs as the original execution.
75
- await rehydrateCompletedStepOutputs(workflow.definition, run.id, ctx, registry);
76
-
77
- // Deep-interpolate config against the node's declared-inputs context —
78
- // the raw run context has no `inputs` aliases, so interpolating against
79
- // it directly resolves every {{alias}} to "" and (e.g.) a retried
80
- // foreach with `over: "{{items}}"` burns all its retries on schema
81
- // rejections without ever re-dispatching.
82
- const inputCtx = buildNodeInterpolationCtx(node, ctx);
83
- const { value: interpolatedValue, scriptBodyUnresolved } = interpolateNodeConfig(
84
- node,
65
+ // Rebuild `run.id`, upstream outputs and the node's declared-inputs
66
+ // context the way the original execution saw them. Without the alias
67
+ // context a retried foreach with `over: "{{items}}"` burns all its
68
+ // retries on schema rejections without ever re-dispatching.
69
+ const {
70
+ ctx,
85
71
  inputCtx,
86
- );
72
+ config: interpolatedConfig,
73
+ scriptBodyUnresolved,
74
+ } = await rebuildNodeExecutionInput(workflow.definition, run, node, registry);
87
75
  if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
88
76
  await checkpointStepFailure(
89
77
  run.id,
@@ -93,7 +81,6 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
93
81
  );
94
82
  continue;
95
83
  }
96
- const interpolatedConfig = interpolatedValue as Record<string, unknown>;
97
84
 
98
85
  // Get executor and re-run
99
86
  const executor = registry.get(node.type);
@@ -126,6 +113,34 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
126
113
  step.retryCount,
127
114
  retryPolicy,
128
115
  );
116
+ } else if (
117
+ "async" in result &&
118
+ (result as AsyncExecutorResult).async &&
119
+ (result as AsyncExecutorResult).waitFor === FOREACH_ITEMS_WAIT_FOR
120
+ ) {
121
+ // A retried foreach materialized its children: park it and start
122
+ // them, as executeStep does. An all-inline fan-out can close its
123
+ // join inside the drive; route the successors here then.
124
+ const drive = await parkAndDriveForeach({
125
+ runId: run.id,
126
+ stepId: step.id,
127
+ parentNodeId: node.id,
128
+ ctx,
129
+ registry,
130
+ });
131
+ if (!drive.joined) continue;
132
+ if (drive.successors.length > 0) {
133
+ await walkGraph(
134
+ workflow.definition,
135
+ run.id,
136
+ ctx,
137
+ drive.successors,
138
+ registry,
139
+ workflow.id,
140
+ );
141
+ } else {
142
+ await completeRunIfSettled(run.id, ctx);
143
+ }
129
144
  } else if ("async" in result && (result as AsyncExecutorResult).async) {
130
145
  // An async executor (agent-task, foreach) re-dispatched its work —
131
146
  // the step is waiting on task events again, exactly like the
@@ -0,0 +1,59 @@
1
+ import type { WorkflowDefinition, WorkflowNode, WorkflowRun } from "../types";
2
+ import {
3
+ buildNodeInterpolationCtx,
4
+ interpolateNodeConfig,
5
+ rehydrateCompletedStepOutputs,
6
+ } from "./engine";
7
+ import type { ExecutorRegistry } from "./executors/registry";
8
+
9
+ /** A node's execution input rebuilt outside the live walk that first ran it. */
10
+ export interface NodeExecutionInput {
11
+ /** The run context, hydrated with `run.id` and every completed step output. */
12
+ ctx: Record<string, unknown>;
13
+ /** The node's declared-inputs interpolation context (`meta.inputCtx`). */
14
+ inputCtx: Record<string, unknown>;
15
+ /** The node config interpolated against `inputCtx`. */
16
+ config: Record<string, unknown>;
17
+ unresolved: string[];
18
+ scriptBodyUnresolved?: string[];
19
+ strictUnresolved?: string[];
20
+ }
21
+
22
+ /**
23
+ * Rebuild a node's interpolated config and input context from persisted run
24
+ * state, the way the live walk built them. The retry poller and the foreach
25
+ * driver run executors outside walkGraph and go through here.
26
+ *
27
+ * The persisted run context can lag behind completed step checkpoints, and a
28
+ * step can fail before ANY checkpoint persisted the walkGraph-hydrated
29
+ * context, so `run.id` may be absent: both are restored before resolving the
30
+ * node's declared inputs. Interpolating against the raw run context instead
31
+ * resolves every `{{alias}}` to "" (it has no `inputs` aliases).
32
+ *
33
+ * Pass `ctx` to reuse a live walk's already hydrated context as-is.
34
+ */
35
+ export async function rebuildNodeExecutionInput(
36
+ def: WorkflowDefinition,
37
+ run: Pick<WorkflowRun, "id" | "context">,
38
+ node: WorkflowNode,
39
+ registry: ExecutorRegistry,
40
+ options: { ctx?: Record<string, unknown> } = {},
41
+ ): Promise<NodeExecutionInput> {
42
+ const ctx = options.ctx ?? ((run.context ?? {}) as Record<string, unknown>);
43
+ if (!("run" in ctx)) ctx.run = { id: run.id };
44
+ if (!options.ctx) await rehydrateCompletedStepOutputs(def, run.id, ctx, registry);
45
+
46
+ const inputCtx = buildNodeInterpolationCtx(node, ctx);
47
+ const { value, unresolved, scriptBodyUnresolved, strictUnresolved } = interpolateNodeConfig(
48
+ node,
49
+ inputCtx,
50
+ );
51
+ return {
52
+ ctx,
53
+ inputCtx,
54
+ config: value as Record<string, unknown>,
55
+ unresolved,
56
+ scriptBodyUnresolved,
57
+ strictUnresolved,
58
+ };
59
+ }