@desplega.ai/agent-swarm 2.4.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
  4. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
  35. package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
  37. package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
  38. package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
  44. package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  49. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  50. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  51. package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
  52. package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
  53. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  54. package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
  55. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  56. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  57. package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
  58. package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
  59. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  60. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  61. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  62. package/dist/cli-pkaqk57w.js +128 -0
  63. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  64. package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
  65. package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
  66. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  67. package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
  68. package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
  69. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  70. package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
  71. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  72. package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
  73. package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
  74. package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
  75. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  76. package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
  77. package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
  78. package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
  79. package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
  80. package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
  81. package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
  82. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  83. package/dist/cli.js +31 -29
  84. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  85. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
  86. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  87. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
  88. package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
  89. package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
  90. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
  91. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
  92. package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
  93. package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
  94. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  95. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
  96. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  97. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
  98. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  99. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  100. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  101. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  102. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  103. package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
  104. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  105. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  106. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
  107. package/dist/handlers-c9ajzhbn.js +41 -0
  108. package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
  109. package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
  110. package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
  111. package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
  112. package/dist/index-07ef7nkm.js +42 -0
  113. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  114. package/dist/index-7yts8n9s.js +43 -0
  115. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  116. package/dist/index-q3t40ryv.js +46 -0
  117. package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
  118. package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
  119. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  120. package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
  121. package/dist/lead-aayq64jn.js +79 -0
  122. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  123. package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
  124. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  125. package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
  126. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  127. package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
  128. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  129. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  130. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  131. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  132. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  133. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  134. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
  135. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  136. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  137. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
  138. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
  139. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
  140. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  141. package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
  142. package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
  143. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  144. package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
  145. package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
  146. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  147. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  148. package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
  149. package/dist/worker-nwb789mg.js +79 -0
  150. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  151. package/openapi.json +793 -6
  152. package/package.json +11 -11
  153. package/src/apps/catalog.generated.json +206 -4
  154. package/src/apps/page-validator.ts +13 -1
  155. package/src/assets/key.ts +17 -0
  156. package/src/azure-devops/handlers.ts +1 -1
  157. package/src/be/chunking.ts +42 -9
  158. package/src/be/db/approvals.ts +18 -0
  159. package/src/be/db/session-moves.ts +164 -0
  160. package/src/be/db/session-tree.ts +384 -0
  161. package/src/be/db/tasks/draft-text.ts +15 -0
  162. package/src/be/db/tasks/read.ts +54 -5
  163. package/src/be/db/workflow-runs.ts +96 -0
  164. package/src/be/db.ts +85 -237
  165. package/src/be/memory/index-content.ts +36 -17
  166. package/src/be/memory/providers/openai-embedding.ts +115 -61
  167. package/src/be/memory/providers/sqlite-store.ts +69 -23
  168. package/src/be/memory/reranker.ts +31 -1
  169. package/src/be/memory/search-candidates.ts +47 -0
  170. package/src/be/memory/types.ts +14 -0
  171. package/src/be/onboarding.ts +3 -0
  172. package/src/be/scripts/typecheck.ts +2 -0
  173. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  174. package/src/be/seed-scripts/index.ts +2 -2
  175. package/src/be/skill-sync.ts +43 -2
  176. package/src/be/swarm-config-guard.ts +25 -1
  177. package/src/be/task-attachment-store.ts +30 -0
  178. package/src/bitbucket/api.ts +81 -0
  179. package/src/bitbucket/auth.ts +64 -0
  180. package/src/bitbucket/handlers.ts +434 -0
  181. package/src/bitbucket/index.ts +28 -0
  182. package/src/bitbucket/templates.ts +134 -0
  183. package/src/bitbucket/types.ts +65 -0
  184. package/src/commands/runner.ts +55 -3
  185. package/src/e2b/env.ts +1 -0
  186. package/src/extensions/catalog.generated.json +4 -4
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/contract.ts +9 -2
  189. package/src/extensions/post-bridge.ts +1 -0
  190. package/src/fs/provider.ts +13 -1
  191. package/src/github/handlers.ts +6 -6
  192. package/src/gitlab/handlers.ts +5 -5
  193. package/src/heartbeat/heartbeat.ts +31 -11
  194. package/src/http/assets.ts +14 -2
  195. package/src/http/core.ts +12 -0
  196. package/src/http/fs.ts +54 -34
  197. package/src/http/index.ts +4 -0
  198. package/src/http/mcp-body.ts +37 -0
  199. package/src/http/mcp-user.ts +3 -5
  200. package/src/http/mcp.ts +6 -7
  201. package/src/http/memory.ts +26 -15
  202. package/src/http/onboarding.ts +1 -0
  203. package/src/http/page-proxy.ts +61 -5
  204. package/src/http/pages-public.ts +27 -44
  205. package/src/http/pages.ts +4 -4
  206. package/src/http/script-runs.ts +2 -0
  207. package/src/http/sessions.ts +366 -12
  208. package/src/http/skills.ts +48 -1
  209. package/src/http/tasks.ts +1 -1
  210. package/src/http/users.ts +13 -1
  211. package/src/http/webhooks.ts +141 -0
  212. package/src/integrations/chat-sdk/adapters.ts +24 -4
  213. package/src/integrations/chat-sdk/attachments.ts +90 -12
  214. package/src/integrations/chat-sdk/facts.ts +138 -0
  215. package/src/integrations/chat-sdk/inbound.ts +156 -8
  216. package/src/integrations/chat-sdk/limits.ts +24 -0
  217. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  218. package/src/integrations/chat-sdk/outbound.ts +272 -5
  219. package/src/integrations/chat-sdk/relay.ts +4 -1
  220. package/src/integrations/chat-sdk/router.ts +28 -2
  221. package/src/integrations/chat-sdk/steering.ts +9 -0
  222. package/src/integrations/chat-sdk/templates.ts +14 -0
  223. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  224. package/src/mcp-limits.ts +20 -0
  225. package/src/pages/browser-sdk.ts +8 -3
  226. package/src/pages/feedback-overlay.ts +1 -1
  227. package/src/providers/acp-targets.ts +1 -1
  228. package/src/providers/amp-adapter.ts +1 -1
  229. package/src/providers/codex-adapter.ts +106 -119
  230. package/src/providers/codex-agents-md.ts +35 -229
  231. package/src/providers/cursor-adapter.ts +1 -1
  232. package/src/providers/pi-mono-adapter.ts +5 -53
  233. package/src/realtime/transport.ts +1 -2
  234. package/src/script-workflows/workflow-ctx.ts +1 -0
  235. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  236. package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
  237. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  238. package/src/server-user.ts +2 -0
  239. package/src/server.ts +2 -0
  240. package/src/slack/assistant.ts +2 -2
  241. package/src/slack/files.ts +2 -0
  242. package/src/slack/handlers.test.ts +158 -0
  243. package/src/slack/handlers.ts +23 -32
  244. package/src/slack/inbound-files.ts +39 -6
  245. package/src/slack/thread-buffer.ts +2 -1
  246. package/src/tasks/context-key.ts +61 -1
  247. package/src/tasks/task-terminal-effects.ts +21 -17
  248. package/src/tests/apps-card-grid.test.ts +197 -0
  249. package/src/tests/apps-catalog-additions.test.ts +120 -0
  250. package/src/tests/apps-tree.test.ts +129 -0
  251. package/src/tests/bitbucket-auth.test.ts +156 -0
  252. package/src/tests/bitbucket-handlers.test.ts +425 -0
  253. package/src/tests/bitbucket-webhook.test.ts +220 -0
  254. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  255. package/src/tests/chat-sdk-config.test.ts +11 -0
  256. package/src/tests/chat-sdk-facts.test.ts +23 -0
  257. package/src/tests/chat-sdk-inbound.test.ts +597 -4
  258. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  259. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  260. package/src/tests/codex-adapter.test.ts +167 -242
  261. package/src/tests/config-api-only-keys.test.ts +9 -0
  262. package/src/tests/context-key.test.ts +50 -0
  263. package/src/tests/core-auth.test.ts +5 -0
  264. package/src/tests/cors-allowlist.test.ts +1 -1
  265. package/src/tests/db-task-read-contract.test.ts +7 -0
  266. package/src/tests/db-task-write-contract.test.ts +6 -0
  267. package/src/tests/docs-index-footprint.test.js +3 -3
  268. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  269. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  270. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  271. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  272. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  273. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  274. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  275. package/src/tests/generate-mcp-docs.test.ts +101 -0
  276. package/src/tests/gitlab-handlers.test.ts +3 -3
  277. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  278. package/src/tests/heartbeat-checklist.test.ts +33 -0
  279. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  280. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  281. package/src/tests/integration-disable-flags.test.ts +36 -0
  282. package/src/tests/kv-page-proxy.test.ts +91 -5
  283. package/src/tests/launch-password-rejection.test.ts +2 -2
  284. package/src/tests/mcp-input-caps.test.ts +294 -0
  285. package/src/tests/mcp-tools.test.ts +1 -1
  286. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  287. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  288. package/src/tests/memory.test.ts +14 -0
  289. package/src/tests/onboarding.test.ts +12 -0
  290. package/src/tests/page-guest-session.test.ts +19 -8
  291. package/src/tests/page-proxy-authed.test.ts +1 -1
  292. package/src/tests/page-proxy.test.ts +17 -10
  293. package/src/tests/page-session.test.ts +102 -1
  294. package/src/tests/pages-authed-mode.test.ts +82 -12
  295. package/src/tests/pages-password-mode.test.ts +5 -5
  296. package/src/tests/pi-codemode.test.ts +8 -2
  297. package/src/tests/preload-env.test.ts +57 -0
  298. package/src/tests/preload.ts +21 -0
  299. package/src/tests/prompt-template-remaining.test.ts +38 -0
  300. package/src/tests/realtime-transport.test.ts +13 -7
  301. package/src/tests/run-root-coverage.test.ts +39 -0
  302. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  303. package/src/tests/runner-skills-refresh.test.ts +58 -0
  304. package/src/tests/script-runs-http.test.ts +84 -0
  305. package/src/tests/scripts-http.test.ts +10 -1
  306. package/src/tests/secret-scrubber.test.ts +15 -0
  307. package/src/tests/sessions-move.test.ts +576 -0
  308. package/src/tests/sessions-tree.test.ts +493 -0
  309. package/src/tests/skill-files-http.test.ts +50 -0
  310. package/src/tests/skills-bundle.test.ts +99 -0
  311. package/src/tests/swarm-config-export-to-env.test.ts +23 -3
  312. package/src/tests/task-audio-transcription.test.ts +299 -0
  313. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  314. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  315. package/src/tests/transcription.test.ts +299 -0
  316. package/src/tests/vcs-bitbucket.test.ts +91 -0
  317. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  318. package/src/tests/vcs-provider.test.ts +16 -1
  319. package/src/tests/vcs-tracking.test.ts +3 -3
  320. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  321. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  322. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  323. package/src/tests/workflow-foreach.test.ts +44 -23
  324. package/src/tests/workflow-step-claims.test.ts +6 -6
  325. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  326. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  327. package/src/tests/workflow-tla-races.test.ts +2 -2
  328. package/src/tools/chat-access.ts +10 -0
  329. package/src/tools/chat-post.ts +31 -11
  330. package/src/tools/chat-reply.ts +32 -10
  331. package/src/tools/memory-search.ts +3 -1
  332. package/src/tools/script-delete.ts +1 -1
  333. package/src/tools/system-one-decide.ts +17 -4
  334. package/src/tools/workflows/create-workflow.ts +5 -3
  335. package/src/types.ts +9 -2
  336. package/src/utils/config-env-export.ts +4 -1
  337. package/src/utils/page-session.ts +133 -20
  338. package/src/utils/secret-scrubber.ts +4 -0
  339. package/src/utils/skills-refresh.ts +83 -43
  340. package/src/utils/transcription.ts +246 -0
  341. package/src/vcs/bitbucket.ts +58 -0
  342. package/src/vcs/index.ts +2 -0
  343. package/src/vcs/types.ts +2 -2
  344. package/src/workflows/approval-resolution.ts +4 -1
  345. package/src/workflows/checkpoint.ts +9 -1
  346. package/src/workflows/constants.ts +3 -0
  347. package/src/workflows/definition.ts +48 -17
  348. package/src/workflows/engine.ts +25 -15
  349. package/src/workflows/executors/foreach.ts +45 -99
  350. package/src/workflows/executors/system-one-decision.ts +28 -7
  351. package/src/workflows/executors/system-one-providers.ts +39 -4
  352. package/src/workflows/foreach-body.ts +146 -0
  353. package/src/workflows/foreach-driver.ts +552 -0
  354. package/src/workflows/foreach-join.ts +206 -7
  355. package/src/workflows/node-timeout.ts +21 -0
  356. package/src/workflows/recovery.ts +68 -3
  357. package/src/workflows/resume.ts +221 -43
  358. package/src/workflows/retry-poller.ts +39 -24
  359. package/src/workflows/step-input.ts +59 -0
  360. package/src/workflows/task-step-routing.ts +114 -56
  361. package/templates/extensions/slack-reply-gate/README.md +24 -22
  362. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  363. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  364. package/templates/skills/apps/SKILL.md +28 -6
  365. package/templates/skills/apps/content.md +28 -6
  366. package/templates/skills/chat-sdk/content.md +6 -2
  367. package/templates/skills/code-quality/content.md +2 -1
  368. package/templates/skills/heartbeat-runbook/content.md +5 -5
  369. package/templates/skills/pages/content.md +6 -4
  370. package/templates/skills/slack-interaction/content.md +5 -0
  371. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  372. package/templates/skills/swarm-scripts/content.md +2 -2
  373. package/templates/skills/work-on-task/SKILL.md +1 -1
  374. package/templates/skills/work-on-task/content.md +1 -1
  375. package/dist/cli-51y09c6p.js +0 -2657
  376. package/dist/cli-yw91rnmw.js +0 -13
  377. package/dist/handlers-0pvee9nw.js +0 -39
  378. package/dist/index-gdh8jrdm.js +0 -43
  379. package/dist/index-hg0x2cdv.js +0 -40
  380. package/dist/index-thm0y6a8.js +0 -41
  381. package/dist/lead-ee8vazz2.js +0 -77
  382. package/dist/worker-16s75hm5.js +0 -77
@@ -0,0 +1,146 @@
1
+ import type { WorkflowDefinition, WorkflowNode } from "../types";
2
+ import type { ExecutorRegistry } from "./executors/registry";
3
+ import { resolveForeachParent } from "./foreach-join";
4
+
5
+ /**
6
+ * Executor types a `foreach` body must not use, with the reason. Every other
7
+ * registered executor is a valid body.
8
+ */
9
+ export const FOREACH_BODY_REJECTED: Readonly<Record<string, string>> = {
10
+ foreach: "nested fan-out is not supported: synthetic ids and the join resolve one level only",
11
+ };
12
+
13
+ /**
14
+ * Built-in executors accepted as a `foreach` body, by how one item completes:
15
+ * `inline` items are run and settled by the foreach driver; `parks` items wait
16
+ * on their own resume path (task, child run, approval, wait), which settles
17
+ * the item through `settleForeachChild`. A `system-one-decision` without
18
+ * `humanReview` returns inline.
19
+ *
20
+ * Every built-in executor must appear here or in FOREACH_BODY_REJECTED; the
21
+ * registry exhaustiveness test enforces that a new executor gets a decision.
22
+ */
23
+ export const FOREACH_BODY_ACCEPTED: Readonly<Record<string, "inline" | "parks">> = {
24
+ script: "inline",
25
+ "swarm-script": "inline",
26
+ "raw-llm": "inline",
27
+ notify: "inline",
28
+ "property-match": "inline",
29
+ "code-match": "inline",
30
+ validate: "inline",
31
+ vcs: "inline",
32
+ "system-one-extraction": "inline",
33
+ "agent-task": "parks",
34
+ "sub-workflow": "parks",
35
+ "human-in-the-loop": "parks",
36
+ wait: "parks",
37
+ "system-one-decision": "parks",
38
+ };
39
+
40
+ /**
41
+ * True when `type` may be a foreach body: not rejected, and registered (or a
42
+ * known built-in when no registry is passed).
43
+ */
44
+ export function isForeachBodyType(type: string, registry?: ExecutorRegistry): boolean {
45
+ if (Object.hasOwn(FOREACH_BODY_REJECTED, type)) return false;
46
+ return registry ? registry.has(type) : Object.hasOwn(FOREACH_BODY_ACCEPTED, type);
47
+ }
48
+
49
+ /**
50
+ * The validation error for a foreach body type, or null when it is allowed.
51
+ * Without a registry only the rejection list applies, so a custom executor
52
+ * registered at run time is not refused by a registry-less validation pass.
53
+ */
54
+ export function foreachBodyTypeError(
55
+ nodeId: string,
56
+ type: string,
57
+ registry?: ExecutorRegistry,
58
+ ): string | null {
59
+ const reason = FOREACH_BODY_REJECTED[type];
60
+ if (reason !== undefined) {
61
+ return `Node "${nodeId}" foreach config.body.type "${type}" is not supported: ${reason} (a ${type} body inside foreach "${nodeId}" is rejected)`;
62
+ }
63
+ if (registry && !registry.has(type)) {
64
+ return `Node "${nodeId}" foreach config.body.type "${type}" is not a registered executor`;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * The definition node a step row executes. A real node wins. A synthetic
71
+ * foreach child id (`<foreachId>#<itemKey>`) resolves to a node synthesized
72
+ * from the parent's body: the child's id, the body's type and config, and the
73
+ * parent's declared `inputs` (the body sees the parent's input context, and the
74
+ * script-body allowlist classifies those aliases by their source).
75
+ */
76
+ export function resolveStepNode(
77
+ def: Pick<WorkflowDefinition, "nodes">,
78
+ nodeId: string,
79
+ ): WorkflowNode | null {
80
+ const node = def.nodes.find((candidate) => candidate.id === nodeId);
81
+ if (node) return node;
82
+ const parent = resolveForeachParent(def, nodeId);
83
+ if (!parent) return null;
84
+ const body = parent.config.body;
85
+ if (!isRecord(body) || typeof body.type !== "string") return null;
86
+ // `item` and `index` are bound per item, so a parent alias with either name
87
+ // is dropped: it would otherwise classify item data by the alias's source
88
+ // and let it past the script-body allowlist.
89
+ const inputs = parent.inputs
90
+ ? Object.fromEntries(
91
+ Object.entries(parent.inputs).filter(([alias]) => !FOREACH_ITEM_BINDINGS.has(alias)),
92
+ )
93
+ : undefined;
94
+ return {
95
+ id: nodeId,
96
+ type: body.type,
97
+ config: isRecord(body.config) ? body.config : {},
98
+ ...(inputs ? { inputs } : {}),
99
+ };
100
+ }
101
+
102
+ const FOREACH_ITEM_BINDINGS = new Set(["item", "index"]);
103
+
104
+ export const FOREACH_FAILURE_POLICIES = ["fail-fast", "collect-outcomes"] as const;
105
+ export type ForeachFailurePolicy = (typeof FOREACH_FAILURE_POLICIES)[number];
106
+
107
+ /**
108
+ * How a failed or cancelled item affects its fan-out. `config.failurePolicy`
109
+ * wins; otherwise it follows the workflow's `onNodeFailure`, so a definition
110
+ * without the field behaves as before: `"continue"` collects outcomes, the
111
+ * default `"fail"` is fail-fast.
112
+ */
113
+ export function effectiveFailurePolicy(
114
+ node: WorkflowNode,
115
+ def: Pick<WorkflowDefinition, "onNodeFailure">,
116
+ ): ForeachFailurePolicy {
117
+ const configured = node.config.failurePolicy;
118
+ if (configured === "fail-fast" || configured === "collect-outcomes") return configured;
119
+ return def.onNodeFailure === "continue" ? "collect-outcomes" : "fail-fast";
120
+ }
121
+
122
+ /** How many items of a fan-out may be in flight at once. Unbounded by default. */
123
+ export function foreachConcurrencyCap(node: WorkflowNode): number {
124
+ const cap = node.config.concurrency;
125
+ return typeof cap === "number" && Number.isInteger(cap) && cap >= 1
126
+ ? cap
127
+ : Number.POSITIVE_INFINITY;
128
+ }
129
+
130
+ /**
131
+ * The failure mode a foreach child's task failure takes: the fan-out's
132
+ * effective policy for a synthetic child, the workflow's `onNodeFailure`
133
+ * otherwise.
134
+ */
135
+ export function stepFailureMode(
136
+ def: Pick<WorkflowDefinition, "nodes" | "onNodeFailure">,
137
+ nodeId: string,
138
+ ): "fail" | "continue" {
139
+ const parent = resolveForeachParent(def, nodeId);
140
+ if (parent) return effectiveFailurePolicy(parent, def) === "fail-fast" ? "fail" : "continue";
141
+ return def.onNodeFailure ?? "fail";
142
+ }
143
+
144
+ function isRecord(value: unknown): value is Record<string, unknown> {
145
+ return typeof value === "object" && value !== null && !Array.isArray(value);
146
+ }
@@ -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
+ }