@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
@@ -0,0 +1,552 @@
1
+ import { renderAutomationTokens } from "../be/automation-preflight";
2
+ import {
3
+ detachTaskFromWorkflowRunStep,
4
+ getDbClient,
5
+ getLatestStepForNode,
6
+ getTaskByWorkflowRunStepId,
7
+ getWorkflow,
8
+ getWorkflowRun,
9
+ getWorkflowRunStep,
10
+ getWorkflowRunStepsByRunId,
11
+ updateWorkflowRun,
12
+ updateWorkflowRunStep,
13
+ } from "../be/db";
14
+ import type {
15
+ Workflow,
16
+ WorkflowDefinition,
17
+ WorkflowNode,
18
+ WorkflowRun,
19
+ WorkflowRunStep,
20
+ } from "../types";
21
+ import { checkpointStepWaiting } from "./checkpoint";
22
+ import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
23
+ import {
24
+ hasRunningStep,
25
+ holdWorkflowRun,
26
+ interpolateNodeConfig,
27
+ scriptBodyInterpolationError,
28
+ walkGraph,
29
+ } from "./engine";
30
+ import type { AsyncExecutorResult } from "./executors/base";
31
+ import { ForeachConfigSchema } from "./executors/foreach";
32
+ import type { ExecutorRegistry } from "./executors/registry";
33
+ import { systemOneUnresolvedError } from "./executors/system-one-decision";
34
+ import {
35
+ effectiveFailurePolicy,
36
+ foreachConcurrencyCap,
37
+ isForeachBodyType,
38
+ resolveStepNode,
39
+ } from "./foreach-body";
40
+ import {
41
+ cancelQueuedForeachChildren,
42
+ FOREACH_TERMINAL_STEP_STATUSES,
43
+ type ForeachChildOutcome,
44
+ latestForeachChildren,
45
+ settleForeachChild,
46
+ } from "./foreach-join";
47
+ import { getSecretInputKeys } from "./input";
48
+ import { resolveNodeTimeoutMs } from "./node-timeout";
49
+ import { rebuildNodeExecutionInput } from "./step-input";
50
+
51
+ export { FOREACH_ITEMS_WAIT_FOR };
52
+
53
+ /**
54
+ * Child step ids a drive in this process is starting right now. A `running`
55
+ * child outside this set has no owner: a crash (or a lost drive) left it
56
+ * between its claim and its settlement, and the next drive restarts it.
57
+ */
58
+ const ownedChildren = new Set<string>();
59
+
60
+ /** Fire-and-forget drives, tracked so tests and shutdown can drain them. */
61
+ const activeDrives = new Set<Promise<void>>();
62
+
63
+ export interface ForeachDriveResult {
64
+ /** True when a settlement in this drive closed the join. */
65
+ joined: boolean;
66
+ /** The parent's successors to walk, when `joined`. */
67
+ successors: WorkflowNode[];
68
+ /** How many children this drive claimed (queued or orphaned). */
69
+ claimed: number;
70
+ }
71
+
72
+ const NOT_JOINED: ForeachDriveResult = { joined: false, successors: [], claimed: 0 };
73
+
74
+ export function isForeachChildOwned(stepId: string): boolean {
75
+ return ownedChildren.has(stepId);
76
+ }
77
+
78
+ /** Drop in-process child ownership, as a restart would. Tests only. */
79
+ export function _resetForeachOwnershipForTests(): void {
80
+ ownedChildren.clear();
81
+ }
82
+
83
+ /**
84
+ * Park a foreach parent whose executor materialized its children, then drive
85
+ * them. The run stays `running` while the drive starts the children; when the
86
+ * join stays open it parks as `waiting` like any async step. Shared by
87
+ * executeStep and the retry poller.
88
+ */
89
+ export async function parkAndDriveForeach(input: {
90
+ runId: string;
91
+ stepId: string;
92
+ parentNodeId: string;
93
+ ctx: Record<string, unknown>;
94
+ registry: ExecutorRegistry;
95
+ }): Promise<ForeachDriveResult & { parked: boolean }> {
96
+ // A re-walk of an open fan-out (a run retry, recovery) reuses a parent step
97
+ // that is already parked: drive it as it is.
98
+ const parked =
99
+ (await checkpointStepWaiting(input.runId, input.stepId, input.ctx, {
100
+ keepRunStatus: true,
101
+ })) || (await getWorkflowRunStep(input.stepId))?.status === "waiting";
102
+ if (!parked) return { ...NOT_JOINED, parked: false };
103
+ const drive = await driveForeach(input.runId, input.parentNodeId, input.registry, {
104
+ ctx: input.ctx,
105
+ });
106
+ if (!drive.joined) await parkRunIfRunning(input.runId);
107
+ return { ...drive, parked: true };
108
+ }
109
+
110
+ /**
111
+ * Claim and start the children of foreach `parentNodeId` that are free to
112
+ * run: queued (`pending`) children up to the cap, plus orphaned `running`
113
+ * children no drive owns. Instant bodies run and settle here, each against its
114
+ * own watchdog budget. Async bodies park, and their own resume path settles
115
+ * them through `settleForeachChild`.
116
+ *
117
+ * Idempotent: the claim flips each child out of `pending` in a `BEGIN
118
+ * IMMEDIATE` transaction, and every async executor dedupes on the child step
119
+ * id, so a repeated drive never duplicates a task, child run or approval.
120
+ */
121
+ export async function driveForeach(
122
+ runId: string,
123
+ parentNodeId: string,
124
+ registry: ExecutorRegistry,
125
+ options: { ctx?: Record<string, unknown> } = {},
126
+ ): Promise<ForeachDriveResult> {
127
+ const run = await getWorkflowRun(runId);
128
+ if (!run || (run.status !== "running" && run.status !== "waiting")) return NOT_JOINED;
129
+ const workflow = await getRenderedWorkflow(run.workflowId);
130
+ if (!workflow) return NOT_JOINED;
131
+ const def = workflow.definition;
132
+ const parentNode = def.nodes.find((node) => node.id === parentNodeId);
133
+ if (!parentNode || parentNode.type !== "foreach") return NOT_JOINED;
134
+ const parentStep = await getLatestStepForNode(runId, parentNodeId);
135
+ if (!parentStep || FOREACH_TERMINAL_STEP_STATUSES.has(parentStep.status)) return NOT_JOINED;
136
+
137
+ const body = parentNode.config.body as { type?: unknown } | undefined;
138
+ const bodyType = typeof body?.type === "string" ? body.type : "";
139
+ // Rebuilt once, on the first claim: most drives (a settlement with nothing
140
+ // queued behind it) claim nothing and never pay for the context rebuild.
141
+ let parentInput: Awaited<ReturnType<typeof loadParentInput>> | undefined;
142
+ let claimedTotal = 0;
143
+ const notJoined = (): ForeachDriveResult => ({ ...NOT_JOINED, claimed: claimedTotal });
144
+
145
+ for (;;) {
146
+ const current = await getWorkflowRun(runId);
147
+ if (!current || (current.status !== "running" && current.status !== "waiting")) {
148
+ return notJoined();
149
+ }
150
+ const claimed = await claimChildren(def, runId, parentNode);
151
+ if (claimed.length === 0) return notJoined();
152
+ claimedTotal += claimed.length;
153
+ const started = new Set<string>();
154
+ let failedFast: string | undefined;
155
+ try {
156
+ parentInput ??= await loadParentInput(def, run, parentNode, registry, workflow.id, options);
157
+ const { ctx, items, itemKeyField, meta } = parentInput;
158
+ for (const child of claimed) {
159
+ // An earlier body in this batch can run for a long time. A cancel (of
160
+ // the run or this child) that landed meanwhile must stop the rest of
161
+ // the batch before it reaches an executor. A fail-fast failure of a
162
+ // sibling elsewhere ends the run too; `finally` cancels the rest.
163
+ const liveness = await childLiveness(runId, child.id);
164
+ if (liveness === "run-ended") return notJoined();
165
+ if (liveness === "child-ended") continue;
166
+ started.add(child.id);
167
+ const outcome = await startChild({
168
+ def,
169
+ child,
170
+ registry,
171
+ ctx,
172
+ meta,
173
+ bodyType,
174
+ item: items.find((candidate) => candidate[itemKeyField] === childItemKey(child)),
175
+ });
176
+ if (!outcome) continue;
177
+ const settled = await settleForeachChild(def, runId, child.id, outcome, ctx, {
178
+ from: ["running"],
179
+ });
180
+ ownedChildren.delete(child.id);
181
+ if (settled.joined) {
182
+ return { joined: true, successors: settled.successors, claimed: claimedTotal };
183
+ }
184
+ if (settled.runFailed) {
185
+ failedFast = child.nodeId;
186
+ return notJoined();
187
+ }
188
+ }
189
+ } finally {
190
+ // A child claimed but never started (an earlier sibling failed the run,
191
+ // or the input rebuild threw) goes back to the queue, so a run retry
192
+ // starts it. After a fail-fast failure it is cancelled with the rest of
193
+ // the queue instead: it never started.
194
+ for (const child of claimed) {
195
+ ownedChildren.delete(child.id);
196
+ if (!started.has(child.id)) await requeueChild(child.id);
197
+ }
198
+ if (
199
+ effectiveFailurePolicy(parentNode, def) === "fail-fast" &&
200
+ (failedFast || (await getWorkflowRun(runId))?.status === "failed")
201
+ ) {
202
+ await cancelQueuedForeachChildren(def, runId, parentNodeId, failedFast);
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * The parent's execution input, rebuilt from persisted state the way the walk
210
+ * built it. Items are looked up by key in the re-interpolated `over`, never
211
+ * persisted on the child row, so resolved secrets stay out of step rows.
212
+ */
213
+ async function loadParentInput(
214
+ def: WorkflowDefinition,
215
+ run: WorkflowRun,
216
+ parentNode: WorkflowNode,
217
+ registry: ExecutorRegistry,
218
+ workflowId: string,
219
+ options: { ctx?: Record<string, unknown> },
220
+ ) {
221
+ const input = await rebuildNodeExecutionInput(def, run, parentNode, registry, {
222
+ ctx: options.ctx,
223
+ });
224
+ const parsed = ForeachConfigSchema.safeParse(input.config);
225
+ return {
226
+ ctx: input.ctx,
227
+ items: parsed.success ? parsed.data.over : [],
228
+ itemKeyField: parsed.success ? parsed.data.itemKey : "",
229
+ meta: {
230
+ runId: run.id,
231
+ workflowId,
232
+ dryRun: false,
233
+ requestedByUserId: run.createdBy,
234
+ inputCtx: input.inputCtx,
235
+ },
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Start a drive without awaiting it. When it closes the join, it walks the
241
+ * parent's successors (or finalizes); otherwise it parks the run if nothing is
242
+ * left running. Errors are logged.
243
+ */
244
+ export function startForeachDrive(
245
+ runId: string,
246
+ parentNodeId: string,
247
+ registry: ExecutorRegistry,
248
+ ): void {
249
+ const drive = runForeachDrive(runId, parentNodeId, registry)
250
+ .then(() => undefined)
251
+ .catch((err) => {
252
+ console.error(`[workflows] foreach drive of ${parentNodeId} in run ${runId} failed:`, err);
253
+ })
254
+ .finally(() => {
255
+ activeDrives.delete(drive);
256
+ });
257
+ activeDrives.add(drive);
258
+ }
259
+
260
+ /**
261
+ * Drive a fan-out outside a live walk and route what it closes. Holds the run
262
+ * for the duration so the running-run recovery leaves it alone. Returns true
263
+ * when the drive started or settled at least one child.
264
+ */
265
+ export async function runForeachDrive(
266
+ runId: string,
267
+ parentNodeId: string,
268
+ registry: ExecutorRegistry,
269
+ ): Promise<ForeachDriveResult> {
270
+ const release = holdWorkflowRun(runId);
271
+ try {
272
+ const drive = await driveForeach(runId, parentNodeId, registry);
273
+ if (!drive.joined) {
274
+ await parkRunIfRunning(runId);
275
+ return drive;
276
+ }
277
+ const run = await getWorkflowRun(runId);
278
+ const workflow = run ? await getRenderedWorkflow(run.workflowId) : null;
279
+ if (!run || !workflow) return drive;
280
+ const ctx = (run.context ?? {}) as Record<string, unknown>;
281
+ if (drive.successors.length > 0) {
282
+ await walkGraph(
283
+ workflow.definition,
284
+ runId,
285
+ ctx,
286
+ drive.successors,
287
+ registry,
288
+ workflow.id,
289
+ getSecretInputKeys(workflow.input),
290
+ );
291
+ } else {
292
+ const { finalizeOrWait } = await import("./resume");
293
+ await finalizeOrWait(runId);
294
+ }
295
+ return drive;
296
+ } finally {
297
+ release();
298
+ }
299
+ }
300
+
301
+ /** Wait for every fire-and-forget drive, including drives they start. */
302
+ export async function drainForeachDrives(): Promise<void> {
303
+ while (activeDrives.size > 0) {
304
+ await Promise.allSettled([...activeDrives]);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * The run's workflow with its install params rendered in, as
310
+ * startWorkflowExecution rendered it before the walk. The driver and its
311
+ * post-join walk reload the definition, so without this a `{{PARAM}}` token in
312
+ * the body config or in `over` would reach the per-item pass unresolved.
313
+ */
314
+ async function getRenderedWorkflow(workflowId: string): Promise<Workflow | null> {
315
+ const workflow = await getWorkflow(workflowId);
316
+ return workflow ? renderAutomationTokens(workflow, workflow.params ?? {}) : null;
317
+ }
318
+
319
+ /** Whether a claimed child may still start: its run is live and its row is still claimed. */
320
+ async function childLiveness(
321
+ runId: string,
322
+ stepId: string,
323
+ ): Promise<"live" | "run-ended" | "child-ended"> {
324
+ const run = await getWorkflowRun(runId);
325
+ if (!run || (run.status !== "running" && run.status !== "waiting")) return "run-ended";
326
+ const step = await getWorkflowRunStep(stepId);
327
+ return step?.status === "running" ? "live" : "child-ended";
328
+ }
329
+
330
+ async function requeueChild(stepId: string): Promise<void> {
331
+ await getDbClient().run(
332
+ "UPDATE workflow_run_steps SET status = 'pending' WHERE id = ? AND status = 'running'",
333
+ [stepId],
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Park a `running` run as `waiting` once nothing in it is executing and a step
339
+ * is waiting. Never completes the run: a drive that did not close the join
340
+ * must not finalize a run whose joiner is still walking the successors.
341
+ */
342
+ async function parkRunIfRunning(runId: string): Promise<void> {
343
+ await getDbClient().transaction(async () => {
344
+ const run = await getWorkflowRun(runId);
345
+ if (run?.status !== "running") return;
346
+ const steps = await getWorkflowRunStepsByRunId(runId);
347
+ if (hasRunningStep(steps)) return;
348
+ if (!steps.some((step) => step.status === "waiting")) return;
349
+ await updateWorkflowRun(runId, { status: "waiting" });
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Flip `pending` children to `running`, in item order, while fewer than the
355
+ * fan-out's `concurrency` cap are in flight (`running` or `waiting`, latest
356
+ * attempt rows only), and collect unowned `running` ones. Ownership is registered before commit, so no other
357
+ * drive can see a claimed row without also seeing it owned.
358
+ */
359
+ async function claimChildren(
360
+ def: WorkflowDefinition,
361
+ runId: string,
362
+ parentNode: WorkflowNode,
363
+ ): Promise<WorkflowRunStep[]> {
364
+ const claimed: WorkflowRunStep[] = [];
365
+ try {
366
+ // The in-flight count and the claim share one BEGIN IMMEDIATE transaction,
367
+ // so concurrent drives (a settlement's drive and the recovery sweep)
368
+ // serialize here and never exceed the cap together.
369
+ await getDbClient().transaction(async () => {
370
+ const children = latestForeachChildren(
371
+ def,
372
+ parentNode.id,
373
+ await getWorkflowRunStepsByRunId(runId),
374
+ );
375
+ for (const child of children) {
376
+ if (child.status === "running" && !ownedChildren.has(child.id)) {
377
+ ownedChildren.add(child.id);
378
+ claimed.push(child);
379
+ }
380
+ }
381
+ const inFlight = children.filter(
382
+ (child) => child.status === "running" || child.status === "waiting",
383
+ ).length;
384
+ let free = foreachConcurrencyCap(parentNode) - inFlight;
385
+ for (const child of children) {
386
+ if (free <= 0) break;
387
+ if (child.status !== "pending") continue;
388
+ const row = await getDbClient().get<{ id: string }>(
389
+ "UPDATE workflow_run_steps SET status = 'running' WHERE id = ? AND status = 'pending' RETURNING id",
390
+ [child.id],
391
+ );
392
+ if (!row) continue;
393
+ ownedChildren.add(child.id);
394
+ claimed.push({ ...child, status: "running" });
395
+ free--;
396
+ }
397
+ });
398
+ } catch (err) {
399
+ for (const child of claimed) ownedChildren.delete(child.id);
400
+ throw err;
401
+ }
402
+ return claimed;
403
+ }
404
+
405
+ /**
406
+ * Run one claimed child's body. Returns its terminal outcome, or null when it
407
+ * parked (or another handler moved it on).
408
+ */
409
+ async function startChild(input: {
410
+ def: WorkflowDefinition;
411
+ child: WorkflowRunStep;
412
+ registry: ExecutorRegistry;
413
+ ctx: Record<string, unknown>;
414
+ meta: {
415
+ runId: string;
416
+ workflowId: string;
417
+ dryRun: boolean;
418
+ requestedByUserId?: string;
419
+ inputCtx: Record<string, unknown>;
420
+ };
421
+ bodyType: string;
422
+ item: Record<string, unknown> | undefined;
423
+ }): Promise<ForeachChildOutcome | null> {
424
+ const { def, child, registry, ctx, meta, bodyType, item } = input;
425
+ if (!item) {
426
+ return {
427
+ status: "failed",
428
+ error: `item "${childItemKey(child)}" is no longer present in over`,
429
+ };
430
+ }
431
+ if (!isForeachBodyType(bodyType, registry)) {
432
+ return { status: "failed", error: `foreach body type "${bodyType}" cannot run` };
433
+ }
434
+ const bodyNode = resolveStepNode(def, child.nodeId);
435
+ if (!bodyNode) return { status: "failed", error: `foreach child ${child.nodeId} has no body` };
436
+
437
+ // The per-item pass interpolates against the parent's declared-inputs
438
+ // context plus item/index, NOT the full run context, so the body obeys the
439
+ // same explicit-dataflow boundary as a normal node. A script body keeps the
440
+ // script-source allowlist: `item` reaches it through `args`, never source.
441
+ const index = childIndex(child);
442
+ const bodyCtx = { ...meta.inputCtx, item, index };
443
+ const interpolation = interpolateNodeConfig(bodyNode, bodyCtx);
444
+ if (interpolation.scriptBodyUnresolved && interpolation.scriptBodyUnresolved.length > 0) {
445
+ return {
446
+ status: "failed",
447
+ error: scriptBodyInterpolationError(child.nodeId, interpolation.scriptBodyUnresolved),
448
+ };
449
+ }
450
+ if (interpolation.strictUnresolved && interpolation.strictUnresolved.length > 0) {
451
+ return {
452
+ status: "failed",
453
+ error: systemOneUnresolvedError(child.nodeId, interpolation.strictUnresolved, bodyType),
454
+ };
455
+ }
456
+ if (interpolation.unresolved.length > 0) {
457
+ console.warn(
458
+ `[workflow] Step ${child.nodeId}: unresolved interpolation tokens: ${interpolation.unresolved.join(", ")}`,
459
+ );
460
+ await updateWorkflowRunStep(child.id, {
461
+ diagnostics: JSON.stringify({ unresolvedTokens: interpolation.unresolved }),
462
+ });
463
+ }
464
+ const config = interpolation.value as Record<string, unknown>;
465
+
466
+ if (bodyType === "agent-task") {
467
+ // A retried item needs a fresh task. Detaching the stale terminal task keeps
468
+ // the executor's one-task-per-step de-duplication deterministic and scoped.
469
+ const linkedTask = await getTaskByWorkflowRunStepId(child.id);
470
+ if (linkedTask?.status === "failed" || linkedTask?.status === "cancelled") {
471
+ await detachTaskFromWorkflowRunStep(linkedTask.id);
472
+ }
473
+ }
474
+
475
+ const executor = registry.get(bodyType);
476
+ const timeoutMs = resolveNodeTimeoutMs({ type: bodyType, config });
477
+ let timer: ReturnType<typeof setTimeout> | undefined;
478
+ let result: Awaited<ReturnType<typeof executor.run>>;
479
+ try {
480
+ result = await Promise.race([
481
+ executor.run({
482
+ config,
483
+ context: { ...ctx, ...bodyCtx },
484
+ meta: { ...meta, stepId: child.id, nodeId: child.nodeId },
485
+ }),
486
+ new Promise<never>((_, reject) => {
487
+ timer = setTimeout(
488
+ () => reject(new Error(`Step timed out after ${timeoutMs}ms`)),
489
+ timeoutMs,
490
+ );
491
+ }),
492
+ ]);
493
+ } catch (err) {
494
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
495
+ } finally {
496
+ if (timer) clearTimeout(timer);
497
+ }
498
+
499
+ if (result.status === "failed") {
500
+ return {
501
+ status: "failed",
502
+ error: result.error || "Executor returned failed status",
503
+ ...(result.output !== undefined ? { output: result.output } : {}),
504
+ };
505
+ }
506
+
507
+ if ("async" in result && (result as AsyncExecutorResult).async) {
508
+ const parked = await getDbClient().get<{ id: string }>(
509
+ "UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id",
510
+ [child.id],
511
+ );
512
+ // A child run that finished before the item parked had its event dropped.
513
+ if (parked && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
514
+ ownedChildren.delete(child.id);
515
+ const { resumeFromChildRun } = await import("./resume");
516
+ await resumeFromChildRun(child.id, registry);
517
+ }
518
+ return null;
519
+ }
520
+
521
+ return {
522
+ status: "completed",
523
+ output: bodyType === "agent-task" ? normalizeExistingTaskOutput(result.output) : result.output,
524
+ ...(result.nextPort ? { nextPort: result.nextPort } : {}),
525
+ };
526
+ }
527
+
528
+ function childItemKey(child: WorkflowRunStep): string | undefined {
529
+ const input = child.input as Record<string, unknown> | null | undefined;
530
+ return typeof input?.itemKey === "string" ? input.itemKey : undefined;
531
+ }
532
+
533
+ function childIndex(child: WorkflowRunStep): number {
534
+ const input = child.input as Record<string, unknown> | null | undefined;
535
+ return typeof input?.index === "number" ? input.index : 0;
536
+ }
537
+
538
+ /** An agent-task body that reconnected to an already-completed task. */
539
+ function normalizeExistingTaskOutput(output: unknown): unknown {
540
+ if (typeof output !== "object" || output === null) return output;
541
+ const record = output as Record<string, unknown>;
542
+ if (typeof record.taskOutput !== "string") return output;
543
+ try {
544
+ const parsed = JSON.parse(record.taskOutput);
545
+ if (typeof parsed === "object" && parsed !== null) {
546
+ return { ...record, taskOutput: parsed };
547
+ }
548
+ } catch {
549
+ // Non-structured task output remains a string.
550
+ }
551
+ return output;
552
+ }