@desplega.ai/agent-swarm 2.5.3 → 2.6.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 (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -7,6 +7,7 @@ import {
7
7
  updateWorkflowRun,
8
8
  updateWorkflowRunStep,
9
9
  } from "../be/db";
10
+ import { getRunLineageBlock } from "../be/db/workflow-runs";
10
11
  import type {
11
12
  WorkflowDefinition,
12
13
  WorkflowNode,
@@ -27,6 +28,22 @@ export const FOREACH_TERMINAL_STEP_STATUSES = new Set([
27
28
  "skipped",
28
29
  ]);
29
30
 
31
+ /**
32
+ * A failed step queued for the retry poller (`nextRetryAt` set). A foreach
33
+ * child in this state is still in flight: it is not settled and holds its
34
+ * concurrency slot until the poller re-queues it.
35
+ */
36
+ export function isRetryPending(step: Pick<WorkflowRunStep, "status" | "nextRetryAt">): boolean {
37
+ return step.status === "failed" && step.nextRetryAt != null;
38
+ }
39
+
40
+ /** A foreach child whose outcome is final: terminal and not queued for a retry. */
41
+ export function isForeachChildSettled(
42
+ step: Pick<WorkflowRunStep, "status" | "nextRetryAt">,
43
+ ): boolean {
44
+ return FOREACH_TERMINAL_STEP_STATUSES.has(step.status) && !isRetryPending(step);
45
+ }
46
+
30
47
  export interface SyntheticNodeId {
31
48
  parentNodeId: string;
32
49
  itemKey: string;
@@ -74,8 +91,9 @@ export async function joinForeach(
74
91
 
75
92
  const children = latestForeachChildren(def, parent.id, await getWorkflowRunStepsByRunId(runId));
76
93
 
77
- // `pending` (queued, not started) is non-terminal like running and waiting.
78
- if (children.some((step) => !FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
94
+ // `pending` (queued, not started) and a child queued for a retry are
95
+ // non-terminal like running and waiting.
96
+ if (children.some((step) => !isForeachChildSettled(step))) {
79
97
  return { joined: false, parentNodeId: parent.id, successors: [] };
80
98
  }
81
99
 
@@ -244,6 +262,8 @@ export async function settleForeachChild(
244
262
  });
245
263
 
246
264
  if (run.status !== "running" && run.status !== "waiting") return unclaimed;
265
+ // The outcome stays on the item as history; a cancelled tree routes nothing.
266
+ if (await getRunLineageBlock(runId)) return unclaimed;
247
267
 
248
268
  if (outcome.status !== "completed" && effectiveFailurePolicy(parent, def) === "fail-fast") {
249
269
  const reason = options.runError ?? outcome.error ?? `foreach item ${current.nodeId} failed`;
@@ -270,8 +290,9 @@ export async function settleForeachChild(
270
290
 
271
291
  /**
272
292
  * Fail-fast: cancel every queued (`pending`) child of the fan-out so none of
273
- * them starts. In-flight children are left to finish and record their outcome.
274
- * A run retry gives each cancelled item a new attempt.
293
+ * them starts, and every child queued for a retry so the retry poller does not
294
+ * revive the failed run. In-flight children are left to finish and record their
295
+ * outcome. A run retry gives each cancelled item a new attempt.
275
296
  */
276
297
  export async function cancelQueuedForeachChildren(
277
298
  def: WorkflowDefinition,
@@ -294,9 +315,11 @@ export async function cancelQueuedForeachChildren(
294
315
  await getWorkflowRunStepsByRunId(runId),
295
316
  );
296
317
  for (const child of children) {
297
- if (child.status !== "pending") continue;
318
+ if (child.status !== "pending" && !isRetryPending(child)) continue;
298
319
  const row = await getDbClient().get<{ id: string }>(
299
- "UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ? WHERE id = ? AND status = 'pending' RETURNING id",
320
+ `UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ?, nextRetryAt = NULL
321
+ WHERE id = ? AND (status = 'pending' OR (status = 'failed' AND nextRetryAt IS NOT NULL))
322
+ RETURNING id`,
300
323
  [error, finishedAt, child.id],
301
324
  );
302
325
  if (row) cancelled++;
@@ -12,7 +12,11 @@ import {
12
12
  resolveApprovalRequest,
13
13
  updateWorkflowRun,
14
14
  } from "../be/db";
15
- import { getRunningSubWorkflowSteps, getSettledChildRunParentSteps } from "../be/db/workflow-runs";
15
+ import {
16
+ getRunLineageBlock,
17
+ getRunningSubWorkflowSteps,
18
+ getSettledChildRunParentSteps,
19
+ } from "../be/db/workflow-runs";
16
20
  import type { WorkflowRunStep } from "../types";
17
21
  import { shapeApprovalResolution } from "./approval-resolution";
18
22
  import { loadCompletedStepRouting } from "./completed-step-routing";
@@ -227,6 +231,7 @@ async function completeIfSettled(runId: string, ctx: Record<string, unknown>): P
227
231
  return getDbClient().transaction(async () => {
228
232
  const run = await getWorkflowRun(runId);
229
233
  if (run?.status !== "running") return false;
234
+ if (await getRunLineageBlock(runId)) return false;
230
235
  const steps = await getWorkflowRunStepsByRunId(runId);
231
236
  if (retryPendingNodeIds(steps).size > 0) return false;
232
237
  const latest = new Map<string, WorkflowRunStep>();
@@ -1,6 +1,8 @@
1
1
  import {
2
+ type ApprovalRequest,
2
3
  cancelPendingApprovalRequestsForRun,
3
4
  cancelTask,
5
+ getApprovalRequestById,
4
6
  getCompletedStepNodeIds,
5
7
  getDbClient,
6
8
  getPendingEventWaitNames,
@@ -11,13 +13,25 @@ import {
11
13
  getWorkflowRun,
12
14
  getWorkflowRunStep,
13
15
  getWorkflowRunStepsByRunId,
14
- listCancelledApprovalRequestsForRun,
15
16
  listCancelledApprovalRequestsForStep,
16
17
  resolveWaitState,
17
18
  updateWorkflowRun,
18
19
  updateWorkflowRunStep,
19
20
  } from "../be/db";
20
- import { createForeachAttemptSteps, getChildWorkflowRunId } from "../be/db/workflow-runs";
21
+ import { listUndeliveredCancelledApprovalRequestIds } from "../be/db/approvals";
22
+ import {
23
+ cancelLiveWorkflowTreeRuns,
24
+ cancelOpenWorkflowTreeSteps,
25
+ createForeachAttemptSteps,
26
+ disarmWorkflowTreeRetries,
27
+ getChildWorkflowRunId,
28
+ getOpenWorkflowTreeTaskIds,
29
+ getRunLineageBlock,
30
+ getTaskIdsWithDependents,
31
+ getWorkflowRunStatus,
32
+ getWorkflowRunTreeIds,
33
+ workflowRunTreeHasOpenWork,
34
+ } from "../be/db/workflow-runs";
21
35
  import {
22
36
  type RetryPolicy,
23
37
  RetryPolicySchema,
@@ -48,6 +62,7 @@ import { startForeachDrive } from "./foreach-driver";
48
62
  import {
49
63
  FOREACH_TERMINAL_STEP_STATUSES,
50
64
  type ForeachChildOutcome,
65
+ isRetryPending,
51
66
  parseSyntheticNodeId,
52
67
  resolveForeachParent,
53
68
  settleForeachChild,
@@ -278,12 +293,16 @@ export async function finalizeOrWait(runId: string): Promise<void> {
278
293
  // move a step into `waiting` between the read and the write, and a run
279
294
  // finalized from a stale snapshot strands that branch.
280
295
  await getDbClient().transaction(async () => {
296
+ // A cancel (or another finalizer) that committed after the caller's claim
297
+ // wins: a terminal run is never re-parked or completed from here.
298
+ const status = await getWorkflowRunStatus(runId);
299
+ if (status !== "running" && status !== "waiting") return;
300
+ if (await getRunLineageBlock(runId)) return;
281
301
  const steps = await getWorkflowRunStepsByRunId(runId);
282
302
  // A branch still executing belongs to a live walk, which finalizes the
283
303
  // run itself; its finalizer only acts on a `running` run.
284
304
  if (hasRunningStep(steps)) {
285
- const run = await getWorkflowRun(runId);
286
- if (run?.status === "waiting") await updateWorkflowRun(runId, { status: "running" });
305
+ if (status === "waiting") await updateWorkflowRun(runId, { status: "running" });
287
306
  return;
288
307
  }
289
308
  // A step queued for the retry poller is still live, like a waiting one.
@@ -521,6 +540,10 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
521
540
  const attempts = await getDbClient().transaction(async () => {
522
541
  const current = await getWorkflowRun(runId);
523
542
  if (!current || current.status !== "failed") return null;
543
+ // A failed run inside a cancelled tree keeps its failure; the cancel
544
+ // forbids starting it again.
545
+ const blocked = await getRunLineageBlock(runId);
546
+ if (blocked) throw new Error(`Retry not started, run left failed: ${blocked}`);
524
547
  // Fan-outs with failed or cancelled items get one new attempt row per such
525
548
  // item; completed items are never rerun. Selected from the rows as of this
526
549
  // transaction: an in-flight sibling of a fail-fast fan-out can record its
@@ -531,6 +554,13 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
531
554
  ]);
532
555
  const capError = foreachRetryCapError(currentSteps.length, attempts);
533
556
  if (capError) throw new Error(`Retry not started, run left failed: ${capError}`);
557
+ // An item retry still queued when the run failed gets a new attempt row
558
+ // below. Retire the queued one, or the poller starts the old row beside it.
559
+ for (const step of currentSteps) {
560
+ if (isRetryPending(step) && resolveForeachParent(workflow.definition, step.nodeId)) {
561
+ await updateWorkflowRunStep(step.id, { nextRetryAt: null });
562
+ }
563
+ }
534
564
  // A foreach item retries as a new attempt row instead of a reset: a fresh
535
565
  // step id is a fresh invocation identity for every body type, and the
536
566
  // failed row stays as history.
@@ -639,82 +669,162 @@ function foreachRetryCapError(
639
669
  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`;
640
670
  }
641
671
 
672
+ const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled", "skipped"]);
673
+
674
+ /** What one `cancelWorkflowRunRows` call covered. */
675
+ export interface WorkflowRunCancelResult {
676
+ /** True when this call moved the root run to `cancelled`. */
677
+ cancelled: boolean;
678
+ /**
679
+ * The root and every run below it, root first, for the caller's Slack
680
+ * posts. Also set for a repeat cancel that found nothing open, so posts a
681
+ * failed earlier cancel left undelivered are retried. Empty when the root
682
+ * is missing or completed, failed or skipped.
683
+ */
684
+ runIds: string[];
685
+ }
686
+
642
687
  /**
643
- * The DB writes of a run cancel, with no Slack post. Returns false when the
644
- * run is missing or already terminal. Called inside a caller's transaction,
645
- * the writes join it (as a SAVEPOINT) and commit or roll back with it.
688
+ * The DB writes of a run cancel, with no Slack post. Cancels the run and its
689
+ * whole child tree (every attempt's child, through terminal runs) in one
690
+ * transaction:
691
+ * - every non-terminal step, its task, and the run's other open tasks;
692
+ * - pending approvals and queued step retries;
693
+ * - every non-terminal run. A terminal descendant keeps its status, result
694
+ * and reason; only its leftover open work is cancelled.
695
+ *
696
+ * A missing root, or one that is completed, failed or skipped, cancels
697
+ * nothing. An already cancelled root is walked again, so a repeat cancel
698
+ * finishes descendants an earlier cancel missed. A tree with a cycle or past
699
+ * its bounds throws and nothing commits.
700
+ *
701
+ * Called inside a caller's transaction, the writes join it (as a SAVEPOINT)
702
+ * and commit or roll back with it. Task lifecycle events and worker nudges
703
+ * queue behind COMMIT (afterCommit) and drop on rollback.
646
704
  */
647
- export async function cancelWorkflowRunRows(runId: string, cancelReason: string): Promise<boolean> {
648
- const terminalStatuses = ["completed", "failed", "cancelled", "skipped"];
705
+ export async function cancelWorkflowRunRows(
706
+ runId: string,
707
+ cancelReason: string,
708
+ ): Promise<WorkflowRunCancelResult> {
649
709
  const now = new Date().toISOString();
650
710
 
651
- // Step snapshot, task cancels, and both status writes commit together so a
652
- // step created after the snapshot cannot survive the cancel and a
653
- // concurrent cancel cannot interleave. Task lifecycle events queue behind
654
- // this transaction's COMMIT (afterCommit) and drop on rollback.
655
- return await getDbClient().transaction(async () => {
656
- const current = await getWorkflowRun(runId);
657
- if (!current || terminalStatuses.includes(current.status)) return false;
658
-
659
- // Cancel non-terminal steps and their associated tasks
660
- const steps = await getWorkflowRunStepsByRunId(runId);
661
- for (const step of steps) {
662
- if (terminalStatuses.includes(step.status)) continue;
663
-
664
- // Cancel any task linked to this step
665
- const task = await getTaskByWorkflowRunStepId(step.id);
666
- if (task) {
667
- await cancelTask(task.id, cancelReason);
668
- }
711
+ // A repeat cancel of a tree with nothing left open is answered by reads
712
+ // alone, without the write lock. The guards keep a cancelled tree from
713
+ // gaining new work, so the answer still holds when this returns.
714
+ // Its run ids still go back, so the caller retries Slack posts an earlier
715
+ // cancel could not deliver.
716
+ const settledTree = await getDbClient().transaction(
717
+ async () => {
718
+ if ((await getWorkflowRunStatus(runId)) !== "cancelled") return null;
719
+ const ids = await getWorkflowRunTreeIds(runId);
720
+ return (await workflowRunTreeHasOpenWork(ids)) ? null : ids;
721
+ },
722
+ { readOnly: true },
723
+ );
724
+ if (settledTree) return { cancelled: false, runIds: settledTree };
669
725
 
670
- await updateWorkflowRunStep(step.id, {
671
- status: "cancelled",
672
- error: cancelReason,
673
- finishedAt: now,
674
- });
726
+ // The tree snapshot and every write commit together. A child run, task or
727
+ // step that commits first is in the snapshot; one that commits after sees
728
+ // the cancelled rows in its own guard (getRunLineageBlock and the step
729
+ // claims) and is refused.
730
+ return await getDbClient().transaction(async () => {
731
+ const rootStatus = await getWorkflowRunStatus(runId);
732
+ if (!rootStatus || (TERMINAL_RUN_STATUSES.has(rootStatus) && rootStatus !== "cancelled")) {
733
+ return { cancelled: false, runIds: [] };
675
734
  }
676
735
 
677
- await cancelPendingApprovalRequestsForRun(runId, cancelReason);
678
-
679
- // Mark the run itself as cancelled
680
- await updateWorkflowRun(runId, {
681
- status: "cancelled",
682
- error: cancelReason,
683
- finishedAt: now,
684
- });
685
- return true;
736
+ const runIds = await getWorkflowRunTreeIds(runId);
737
+ // Every step task carries the run link (indexed), and a retry detaches
738
+ // only the step link, so this covers current and earlier attempts' tasks.
739
+ // Dependents are looked up once for the whole tree, and only when needed.
740
+ const openTaskIds = await getOpenWorkflowTreeTaskIds(runIds);
741
+ const withDependents =
742
+ openTaskIds.length > 0 ? await getTaskIdsWithDependents() : new Set<string>();
743
+ for (const taskId of openTaskIds) {
744
+ await cancelTask(taskId, cancelReason, { cascadeDependents: withDependents.has(taskId) });
745
+ }
746
+ await cancelOpenWorkflowTreeSteps(runIds, cancelReason, now);
747
+ await disarmWorkflowTreeRetries(runIds, now);
748
+ for (const id of runIds) await cancelPendingApprovalRequestsForRun(id, cancelReason);
749
+ // Descendants' parent steps were just cancelled, so only the root's own
750
+ // parent (when the root is a child) needs the child-finished wake-up.
751
+ await cancelLiveWorkflowTreeRuns(runIds.slice(1), cancelReason, now);
752
+ if (rootStatus !== "cancelled") {
753
+ await updateWorkflowRun(runId, { status: "cancelled", error: cancelReason, finishedAt: now });
754
+ }
755
+ return { cancelled: rootStatus !== "cancelled", runIds };
686
756
  });
687
757
  }
688
758
 
759
+ // Cancels in flight in this process, by run id. The API process owns the
760
+ // database, so this bounds concurrent walks of one tree.
761
+ const inFlightCancels = new Map<string, Promise<void>>();
762
+
689
763
  /**
690
- * Cancel a workflow run and all its non-terminal steps.
691
- * Also cancels any in-progress tasks spawned by waiting/running steps.
764
+ * Cancel a workflow run and its child tree (see `cancelWorkflowRunRows`), then
765
+ * post the Slack updates for every approval the cancel closed. The posts run
766
+ * after COMMIT, and a failed post is logged without undoing the cancel.
767
+ * Concurrent calls for the same run share one cancel and its result.
692
768
  */
693
- export async function cancelWorkflowRun(runId: string, reason?: string): Promise<void> {
694
- const run = await getWorkflowRun(runId);
769
+ export function cancelWorkflowRun(runId: string, reason?: string): Promise<void> {
770
+ const inFlight = inFlightCancels.get(runId);
771
+ if (inFlight) return inFlight;
772
+ const cancel = cancelWorkflowRunOnce(runId, reason).finally(() => {
773
+ inFlightCancels.delete(runId);
774
+ });
775
+ inFlightCancels.set(runId, cancel);
776
+ return cancel;
777
+ }
778
+
779
+ async function cancelWorkflowRunOnce(runId: string, reason?: string): Promise<void> {
780
+ const run = await getWorkflowRun(runId, "display");
695
781
  if (!run) throw new Error("Workflow run not found");
696
782
 
697
- const terminalStatuses = ["completed", "failed", "cancelled", "skipped"];
698
- if (terminalStatuses.includes(run.status) && run.status !== "cancelled") {
783
+ if (TERMINAL_RUN_STATUSES.has(run.status) && run.status !== "cancelled") {
699
784
  throw new Error(`Cannot cancel run in '${run.status}' state`);
700
785
  }
701
786
 
702
787
  const cancelReason = reason ?? "Cancelled by user";
703
- const applied = await cancelWorkflowRunRows(runId, cancelReason);
788
+ const { cancelled, runIds } = await cancelWorkflowRunRows(runId, cancelReason);
704
789
 
705
- // The migration trigger also catches direct step cancellation. Re-read after
706
- // commit so its rows are included alongside the run-wide sweep.
707
- const cancelledApprovals = await listCancelledApprovalRequestsForRun(runId);
708
- const latestRun = await getWorkflowRun(runId);
709
- if (!applied && latestRun?.status !== "cancelled") {
790
+ const latestRun = await getWorkflowRun(runId, "display");
791
+ if (!cancelled && latestRun?.status !== "cancelled") {
710
792
  throw new Error(`Cannot cancel run in '${latestRun?.status ?? "missing"}' state`);
711
793
  }
712
- await postApprovalCancellationUpdates(
713
- cancelledApprovals,
714
- applied ? cancelReason : (latestRun?.error ?? cancelReason),
794
+ await postTreeApprovalCancellations(
795
+ runIds,
796
+ cancelled ? cancelReason : (latestRun?.error ?? cancelReason),
715
797
  );
716
798
  }
717
799
 
800
+ /**
801
+ * Post the cancellation update of every cancelled approval in the given runs
802
+ * whose Slack post is not delivered yet. Re-read after commit: the migration
803
+ * trigger also cancels the approvals of a directly cancelled step. Each post
804
+ * is claimed, and a failed post releases its claim, so the next cancel of the
805
+ * tree retries it and a delivered post is never repeated.
806
+ */
807
+ export async function postTreeApprovalCancellations(
808
+ runIds: string[],
809
+ reason: string,
810
+ options: { exceptRequestId?: string } = {},
811
+ ): Promise<void> {
812
+ try {
813
+ const approvals: ApprovalRequest[] = [];
814
+ for (const id of await listUndeliveredCancelledApprovalRequestIds(runIds)) {
815
+ if (id === options.exceptRequestId) continue;
816
+ const approval = await getApprovalRequestById(id);
817
+ if (approval) approvals.push(approval);
818
+ }
819
+ if (approvals.length > 0) await postApprovalCancellationUpdates(approvals, reason);
820
+ } catch (err) {
821
+ console.error(
822
+ "[workflows] approval cancellation updates not posted:",
823
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
824
+ );
825
+ }
826
+ }
827
+
718
828
  /** Cancel one HITL step and close any approval request it gates. */
719
829
  export async function cancelWorkflowRunStep(
720
830
  stepId: string,
@@ -993,6 +1103,8 @@ export async function resumeFromChildRun(
993
1103
  const routing = await getDbClient().transaction(async () => {
994
1104
  const current = await getWorkflowRunStep(step.id);
995
1105
  if (!current || current.status !== "waiting") return null;
1106
+ // Inside a cancelled tree a child's end settles nothing and queues no retry.
1107
+ if (await getRunLineageBlock(run.id)) return null;
996
1108
  const childId = await getChildWorkflowRunId(step.id, current.retryCount);
997
1109
  const child = childId ? await getWorkflowRun(childId) : null;
998
1110
  // The child row exists before the step parks, so a missing one was deleted.
@@ -6,6 +6,7 @@ import {
6
6
  getWorkflowRunStepsByRunId,
7
7
  updateWorkflowRun,
8
8
  } from "../be/db";
9
+ import { disarmWorkflowTreeRetries, getRunLineageBlock } from "../be/db/workflow-runs";
9
10
  import type { RetryPolicy } from "../types";
10
11
  import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
11
12
  import { getSuccessors, resolveValidationPort } from "./definition";
@@ -17,7 +18,8 @@ import {
17
18
  } from "./engine";
18
19
  import type { AsyncExecutorResult } from "./executors/base";
19
20
  import type { ExecutorRegistry } from "./executors/registry";
20
- import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach } from "./foreach-driver";
21
+ import { FOREACH_ITEMS_WAIT_FOR, parkAndDriveForeach, runForeachDrive } from "./foreach-driver";
22
+ import { resolveForeachParent } from "./foreach-join";
21
23
  import { rebuildNodeExecutionInput } from "./step-input";
22
24
  import { runStepValidation } from "./validation";
23
25
 
@@ -45,6 +47,19 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
45
47
  const workflow = await getWorkflow(run.workflowId);
46
48
  if (!workflow) continue;
47
49
 
50
+ // A foreach child goes back to its fan-out's queue: the driver
51
+ // rebuilds the per-item config and starts it under the concurrency
52
+ // cap, and the item settles into the join like any other.
53
+ const foreachParent = resolveForeachParent(workflow.definition, step.nodeId);
54
+ if (foreachParent) {
55
+ if (!(await claimRetry(run.id, step.id, { requeueForeachChild: true }))) continue;
56
+ console.log(
57
+ `[workflows] Retrying foreach item ${step.nodeId} (attempt ${step.retryCount}) for run ${step.runId}`,
58
+ );
59
+ await runForeachDrive(run.id, foreachParent.id, registry);
60
+ continue;
61
+ }
62
+
48
63
  // Find the node definition for this step
49
64
  const node = workflow.definition.nodes.find((n) => n.id === step.nodeId);
50
65
  if (!node) continue;
@@ -264,14 +279,44 @@ export function startRetryPoller(registry: ExecutorRegistry, intervalMs = 5000):
264
279
  /**
265
280
  * Flip a retry-pending step to `running` only while it is still retry-pending
266
281
  * and its run is live, and revive a run failed by this step.
282
+ *
283
+ * `requeueForeachChild` flips a foreach child to `pending` instead, for the
284
+ * driver to claim, and clears the cut's error: the join reads `error` on a
285
+ * completed child as a failed item, so a stale one would count the retry's
286
+ * success as a failure.
287
+ *
288
+ * A queued child never revives a failed run: its retry does not settle, so
289
+ * something else failed the run. The queued retry is retired instead, leaving
290
+ * the item `failed` with the cut's error for a run retry to give a new attempt.
291
+ *
292
+ * A run inside a cancelled tree is never revived: its queued retry is dropped.
267
293
  */
268
- async function claimRetry(runId: string, stepId: string): Promise<boolean> {
294
+ async function claimRetry(
295
+ runId: string,
296
+ stepId: string,
297
+ options: { requeueForeachChild?: boolean } = {},
298
+ ): Promise<boolean> {
269
299
  return getDbClient().transaction(async () => {
270
300
  const run = await getWorkflowRun(runId);
271
301
  if (!run || !RETRYABLE_RUN_STATUSES.has(run.status)) return false;
302
+ if (await getRunLineageBlock(runId)) {
303
+ await disarmWorkflowTreeRetries([runId], new Date().toISOString());
304
+ return false;
305
+ }
306
+ if (options.requeueForeachChild && run.status === "failed") {
307
+ await getDbClient().run(
308
+ `UPDATE workflow_run_steps SET nextRetryAt = NULL, finishedAt = ?
309
+ WHERE id = ? AND runId = ? AND status = 'failed' AND nextRetryAt IS NOT NULL`,
310
+ [new Date().toISOString(), stepId, runId],
311
+ );
312
+ return false;
313
+ }
314
+ const set = options.requeueForeachChild
315
+ ? "status = 'pending', nextRetryAt = NULL, error = NULL"
316
+ : "status = 'running', nextRetryAt = NULL";
272
317
  const claimed = await getDbClient().get<{ id: string }>(
273
318
  `UPDATE workflow_run_steps
274
- SET status = 'running', nextRetryAt = NULL
319
+ SET ${set}
275
320
  WHERE id = ? AND runId = ? AND status = 'failed' AND nextRetryAt IS NOT NULL
276
321
  RETURNING id`,
277
322
  [stepId, runId],
@@ -293,6 +338,7 @@ async function completeRunIfSettled(runId: string, ctx: Record<string, unknown>)
293
338
  await getDbClient().transaction(async () => {
294
339
  const run = await getWorkflowRun(runId);
295
340
  if (!run || (run.status !== "running" && run.status !== "waiting")) return;
341
+ if (await getRunLineageBlock(runId)) return;
296
342
  const steps = await getWorkflowRunStepsByRunId(runId);
297
343
  const live = steps.some(
298
344
  (s) =>
@@ -7,6 +7,7 @@ import {
7
7
  updateWorkflowRun,
8
8
  updateWorkflowRunStep,
9
9
  } from "../be/db";
10
+ import { getRunLineageBlock } from "../be/db/workflow-runs";
10
11
  import {
11
12
  type RetryPolicy,
12
13
  RetryPolicySchema,
@@ -18,6 +19,7 @@ import {
18
19
  import { checkpointStep, checkpointStepFailure } from "./checkpoint";
19
20
  import { getSuccessors } from "./definition";
20
21
  import type { ExecutorRegistry } from "./executors/registry";
22
+ import { resolveStepNode } from "./foreach-body";
21
23
  import { type ForeachSettleResult, resolveForeachParent, settleForeachChild } from "./foreach-join";
22
24
 
23
25
  export interface TaskStepRoutingResult {
@@ -146,6 +148,7 @@ export async function checkpointPortStepAndResolveSuccessors(
146
148
  if (!run || (run.status !== "waiting" && run.status !== "running")) {
147
149
  return UNCLAIMED;
148
150
  }
151
+ if (await getRunLineageBlock(runId)) return UNCLAIMED;
149
152
  // A foreach child's port (approval rejected/timeout, wait event/timeout)
150
153
  // is stored for display and still counts as a completed item: the body's
151
154
  // ports do not route, the item's consumer reads the output.
@@ -182,8 +185,8 @@ export async function checkpointPortStepAndResolveSuccessors(
182
185
  * or the failed task is no longer the one bound to the step (it was
183
186
  * already retried and a fresh task now owns it); the caller stops.
184
187
  * - `not-eligible`: no retry applies (no `node.retry` and not a supersede
185
- * cut, not an agent-task node, or retries exhausted); the caller applies
186
- * `onNodeFailure` as before.
188
+ * cut, not an agent-task node, retries exhausted, or a foreach child whose
189
+ * run is no longer live); the caller applies `onNodeFailure` as before.
187
190
  */
188
191
  export type TaskStepRetryOutcome = "scheduled" | "not-claimed" | "not-eligible";
189
192
 
@@ -217,8 +220,13 @@ export const SUPERSEDE_RETRY_POLICY: RetryPolicy = {
217
220
  * only after ownership is confirmed: a stale caller's step snapshot may carry
218
221
  * the counter of a newer attempt.
219
222
  *
220
- * Foreach children (`<foreachId>#<key>`) are not matched here: the retry
221
- * poller resolves nodes by exact id and cannot re-dispatch a child yet.
223
+ * A foreach child (`<foreachId>#<key>`) with an `agent-task` body takes the
224
+ * parent foreach node's `retry`, else the same supersede default. While queued
225
+ * it does not settle, so the join stays open; the retry poller re-queues it and
226
+ * the foreach driver starts it under the fan-out's concurrency cap. A child is
227
+ * queued only while its run is live. Exhausted retries, a run that is no longer
228
+ * live and every other failure settle the item through `onNodeFailure` and the
229
+ * fan-out's failure policy as before.
222
230
  */
223
231
  export async function scheduleTaskStepRetry(
224
232
  def: WorkflowDefinition,
@@ -227,11 +235,14 @@ export async function scheduleTaskStepRetry(
227
235
  taskId: string,
228
236
  reason: string,
229
237
  ): Promise<TaskStepRetryOutcome> {
230
- const node = def.nodes.find((n) => n.id === step.nodeId);
238
+ const foreachParent = resolveForeachParent(def, step.nodeId);
239
+ const node = resolveStepNode(def, step.nodeId);
231
240
  if (!node || node.type !== "agent-task") return "not-eligible";
241
+ // A synthesized child node carries only the body; its retry lives on the parent.
242
+ const declaredRetry = foreachParent ? foreachParent.retry : node.retry;
232
243
  let policy: RetryPolicy;
233
- if (node.retry) {
234
- const parsed = RetryPolicySchema.safeParse(node.retry);
244
+ if (declaredRetry) {
245
+ const parsed = RetryPolicySchema.safeParse(declaredRetry);
235
246
  if (!parsed.success) return "not-eligible";
236
247
  policy = parsed.data;
237
248
  } else {
@@ -243,6 +254,15 @@ export async function scheduleTaskStepRetry(
243
254
  const current = await getWorkflowRunStep(step.id);
244
255
  if (!current || current.status !== "waiting") return "not-claimed";
245
256
  if (!(await isTaskBoundToStep(taskId, step.id))) return "not-claimed";
257
+ // A sibling's fail-fast or another branch can fail the run while the
258
+ // caller awaited. A child queued then would revive the failed run, so the
259
+ // caller records the cut as the item's late outcome instead.
260
+ if (foreachParent) {
261
+ const run = await getWorkflowRun(runId);
262
+ if (run?.status !== "running" && run?.status !== "waiting") return "not-eligible";
263
+ }
264
+ // No retry is queued inside a cancelled tree.
265
+ if (await getRunLineageBlock(runId)) return "not-claimed";
246
266
  if (current.retryCount >= policy.maxRetries) return "not-eligible";
247
267
  await detachTaskFromWorkflowRunStep(taskId);
248
268
  await checkpointStepFailure(runId, step.id, reason, current.retryCount, policy);
@@ -280,6 +300,8 @@ export async function completeTaskStepAndResolveSuccessors(
280
300
  if (!current || current.status !== "waiting") return UNCLAIMED;
281
301
  // A stale snapshot of a step since redriven to a fresh task must not route it.
282
302
  if (ownerTaskId && !(await isTaskBoundToStep(ownerTaskId, step.id))) return UNCLAIMED;
303
+ // A late result inside a cancelled tree routes nothing and revives nothing.
304
+ if (await getRunLineageBlock(runId)) return UNCLAIMED;
283
305
 
284
306
  if (resolveForeachParent(def, step.nodeId)) {
285
307
  // onNodeFailure:"continue" completions persist the failure reason as
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "77abe63716623f28177ebe29679c37b5859ef40e",
2
+ "commit": "c81f2bb62f49b45603e9e702558e1ca22e98fcdd",
3
3
  "excludedSkills": [
4
4
  "feedback"
5
5
  ],
@@ -16,8 +16,8 @@
16
16
  "templates/skills/comms/files/references/ste-rules.md": "6ee2ed5d6b49d2e421b6c9d22310a5b589fa1abe81de06213e4b89fbc1271e74",
17
17
  "templates/skills/comms/files/references/visual-shapes.md": "5e07e9250ad4cdc877af2281525185f0de89c01f90a077463a2c77c8a7b998e8",
18
18
  "templates/skills/delegate-work/config.json": "5b1542eb0be359bb8a17117fcf87917e0607d1b939ae3266ddffacdc14efb281",
19
- "templates/skills/delegate-work/content.md": "c9562bd17846d3595d40172ecfdc6f85bb99eaf23597eb9d5aa9635d5c14d079",
20
- "templates/skills/delegate-work/files/scripts/codex-exec.sh": "be9b33ba111afc8b38c20a4190eae19099025474d4ed3278d944ee0e6b6621af",
19
+ "templates/skills/delegate-work/content.md": "1046da2fbc5603c4deed92beb6d98c443df21e87cf83a7d7b3ce58dbc9d01997",
20
+ "templates/skills/delegate-work/files/scripts/codex-exec.sh": "4524a224fe0657d2e2872d97a70357c4a3ce84d2241bd935ed83febf1fac6e37",
21
21
  "templates/skills/design-docs/config.json": "b78141763541486d67a105e8fa23873c9ffee6a682832c4b7266b8320d55ead7",
22
22
  "templates/skills/design-docs/content.md": "fc860031defbd192c1b8e1c024b46509d78dcf10dbf1b8c93cc4d62396153a52",
23
23
  "templates/skills/design-docs/files/template.md": "72d2cdcf8713c08d525d69892c5fed536107594522e3e799ae44f51df7015a36",
@@ -38,13 +38,13 @@
38
38
  "templates/skills/planning/content.md": "543476e4dbb5c352acd686c3929c45cc992d7d033d06f7e9a30f78009b50785c",
39
39
  "templates/skills/planning/files/template.md": "c0d5ca63c07934546172322bbd1180e2901dc45e27b5d12ce3dc5c30f5e7a98b",
40
40
  "templates/skills/qa/config.json": "c53b01558a691c0343b6270a548e6172af86600a429b789eedb518849f7ea741",
41
- "templates/skills/qa/content.md": "c885095ea814b4c410dbc7aac1e2fde09b94b735d818b6f06153901311a232a0",
41
+ "templates/skills/qa/content.md": "46b1147d8af1a292d9bcd7091385d2c98264f10bb5afe4fb61073113eb0a6574",
42
42
  "templates/skills/qa/files/template.md": "1902e4bb65b19ab58c54ab5b63693ad09adecee69079c2c70f58d7d6c6597896",
43
43
  "templates/skills/questioning/config.json": "da2d5fe6dfdac1915a0476a7063e62ebeb996b84caff07cc7527a55477656a80",
44
44
  "templates/skills/questioning/content.md": "66f826a191e96d0041adca00a7c197a458de8e6fe164c1599b8708098d4109ab",
45
45
  "templates/skills/questioning/files/template.md": "a68d72801a0b10380bf773b07e9d2f8d4c5b4b1c609ce954289e38ffc78cc223",
46
46
  "templates/skills/researching/config.json": "921c062cab599097cc7677d2dad4797ec4d5844d2ee73607fbd417ec3fa487d4",
47
- "templates/skills/researching/content.md": "0e8f98a7a5c75059fba6cd2c908b8268efce13192c1220ba594f2a9e3bf1e60a",
47
+ "templates/skills/researching/content.md": "87ced42d15f1fd63d1b4e8d5ed8161537202f5b05a85df38dea273e9e3d6ad08",
48
48
  "templates/skills/researching/files/template.md": "411d34b98c610ef715edf0f24d09f828ba2d0f65f688e7879ef33a9453007949",
49
49
  "templates/skills/reviewing/config.json": "09c40bb098e9738c60a848995fa9fe7568d158631a089c76ba02c0dd955445c7",
50
50
  "templates/skills/reviewing/content.md": "1bbc285c9ad8cc3312f1ee2c558b04ffee914ffb6f5c63fb3741fcbb3e300fa5",
@@ -130,13 +130,13 @@
130
130
  },
131
131
  {
132
132
  "from": "`${CLAUDE_PLUGIN_ROOT}/skills/delegate-work/scripts/codex-exec.sh`",
133
- "line": 66,
133
+ "line": 70,
134
134
  "skill": "delegate-work",
135
135
  "to": "`bash ~/.claude/skills/delegate-work/scripts/codex-exec.sh`"
136
136
  },
137
137
  {
138
138
  "from": "| codex-exec.sh -m",
139
- "line": 69,
139
+ "line": 73,
140
140
  "skill": "delegate-work",
141
141
  "to": "| bash ~/.claude/skills/delegate-work/scripts/codex-exec.sh -m"
142
142
  },
@@ -178,7 +178,7 @@
178
178
  },
179
179
  {
180
180
  "from": "cc-plugin/base/skills/researching/template.md",
181
- "line": 113,
181
+ "line": 115,
182
182
  "skill": "researching",
183
183
  "to": "template.md"
184
184
  },