@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
@@ -0,0 +1,328 @@
1
+ /** Shared credential probes. This module does not import worker harnesses. */
2
+ import {
3
+ type OpenAICompatibleProvider,
4
+ providerBaseUrl,
5
+ providerForKeyVar,
6
+ } from "@desplega/model-catalog";
7
+ import {
8
+ createScopedFetch,
9
+ deriveDefaultRoute,
10
+ getRouteProvider,
11
+ type ModelRoute,
12
+ } from "@desplega/model-routing";
13
+ import { assertUrlSafe, publicEndpointSsrfOptions } from "../oauth/mcp-wrapper";
14
+ import { getOpenRouterAttributionHeaders, getOpenRouterBaseUrl } from "./openrouter-base-url";
15
+ import { scrubSecrets } from "./secret-scrubber";
16
+
17
+ interface ProbeOptions {
18
+ env?: Record<string, string | undefined>;
19
+ publicEndpoint?: boolean;
20
+ }
21
+
22
+ const LIVE_TEST_TIMEOUT_MS = 5_000;
23
+
24
+ function scrubProbeBody(body: string, init: RequestInit): string {
25
+ const headers = new Headers(init.headers);
26
+ const apiKey =
27
+ headers.get("x-api-key") ?? headers.get("authorization")?.replace(/^Bearer\s+/i, "");
28
+ return scrubSecrets(apiKey ? body.replaceAll(apiKey, "[REDACTED]") : body);
29
+ }
30
+
31
+ export interface LiveKeyResult {
32
+ keyType: string;
33
+ index: number;
34
+ suffix: string;
35
+ ok: boolean;
36
+ error?: string;
37
+ httpStatus?: number;
38
+ latency_ms: number;
39
+ }
40
+
41
+ export interface LiveValidationResult {
42
+ ok: boolean;
43
+ error?: string;
44
+ httpStatus?: number;
45
+ warning?: string;
46
+ keys?: LiveKeyResult[];
47
+ latency_ms: number;
48
+ /** No free live check exists for this credential (cloud routes): report presence only. */
49
+ skipped?: boolean;
50
+ }
51
+
52
+ export async function timedFetch(
53
+ url: string,
54
+ init: RequestInit,
55
+ options: ProbeOptions = {},
56
+ ): Promise<{ ok: boolean; status: number; bodyText: string; latency_ms: number }> {
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(() => controller.abort(), LIVE_TEST_TIMEOUT_MS);
59
+ const start = Date.now();
60
+ try {
61
+ if (options.publicEndpoint) assertUrlSafe(url, publicEndpointSsrfOptions());
62
+ const res = await fetch(url, {
63
+ ...init,
64
+ ...(options.publicEndpoint ? { redirect: "manual" } : {}),
65
+ signal: controller.signal,
66
+ });
67
+ let bodyText = "";
68
+ try {
69
+ bodyText = scrubProbeBody(await res.text(), init);
70
+ } catch {
71
+ // The HTTP status is sufficient when the response body cannot be read.
72
+ }
73
+ return {
74
+ ok: res.ok,
75
+ status: res.status,
76
+ bodyText,
77
+ latency_ms: Date.now() - start,
78
+ };
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ export async function checkAnthropicApiKey(
85
+ apiKey: string,
86
+ options: ProbeOptions = {},
87
+ ): Promise<LiveValidationResult> {
88
+ const r = await timedFetch(
89
+ "https://api.anthropic.com/v1/models",
90
+ {
91
+ method: "GET",
92
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
93
+ },
94
+ options,
95
+ );
96
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
97
+ return {
98
+ ok: false,
99
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
100
+ httpStatus: r.status,
101
+ latency_ms: r.latency_ms,
102
+ };
103
+ }
104
+
105
+ export async function checkOpenAiApiKey(
106
+ apiKey: string,
107
+ options: ProbeOptions = {},
108
+ ): Promise<LiveValidationResult> {
109
+ const r = await timedFetch(
110
+ "https://api.openai.com/v1/models",
111
+ {
112
+ method: "GET",
113
+ headers: { Authorization: `Bearer ${apiKey}` },
114
+ },
115
+ options,
116
+ );
117
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
118
+ return {
119
+ ok: false,
120
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
121
+ httpStatus: r.status,
122
+ latency_ms: r.latency_ms,
123
+ };
124
+ }
125
+
126
+ /** `GET /v1/models` is the call the Grok CLI makes first; it runs no inference. */
127
+ export async function checkXaiApiKey(
128
+ apiKey: string,
129
+ options: ProbeOptions = {},
130
+ ): Promise<LiveValidationResult> {
131
+ const r = await timedFetch(
132
+ "https://api.x.ai/v1/models",
133
+ {
134
+ method: "GET",
135
+ headers: { Authorization: `Bearer ${apiKey}` },
136
+ },
137
+ options,
138
+ );
139
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
140
+ return {
141
+ ok: false,
142
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
143
+ httpStatus: r.status,
144
+ latency_ms: r.latency_ms,
145
+ };
146
+ }
147
+
148
+ /** Fetch a key-authenticated GET and normalize it into a `LiveValidationResult`. */
149
+ async function checkBearerGet(
150
+ url: string,
151
+ init: RequestInit,
152
+ options: ProbeOptions = {},
153
+ ): Promise<LiveValidationResult> {
154
+ const r = await timedFetch(url, { method: "GET", ...init }, options);
155
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
156
+ return {
157
+ ok: false,
158
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
159
+ httpStatus: r.status,
160
+ latency_ms: r.latency_ms,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * OpenRouter's `/models` is public and answers 200 for any key, so the check
166
+ * reads `GET /key` (the key's own info, 401 for a bad key). A custom
167
+ * `OPENROUTER_BASE_URL` without `/key` (404/405) falls back to `/models`.
168
+ */
169
+ export async function checkOpenRouter(
170
+ apiKey: string,
171
+ options: ProbeOptions = {},
172
+ ): Promise<LiveValidationResult> {
173
+ const base = getOpenRouterBaseUrl(options.env);
174
+ const get = (url: string) =>
175
+ checkBearerGet(
176
+ url,
177
+ {
178
+ headers: {
179
+ ...getOpenRouterAttributionHeaders(url, options.env),
180
+ Authorization: `Bearer ${apiKey}`,
181
+ },
182
+ },
183
+ options,
184
+ );
185
+ const keyResult = await get(`${base}/key`);
186
+ if (keyResult.httpStatus === 404 || keyResult.httpStatus === 405) return get(`${base}/models`);
187
+ return keyResult;
188
+ }
189
+
190
+ /** `GET /models` on the official DeepSeek API: lists models, runs no inference. */
191
+ export function checkDeepSeekApiKey(
192
+ apiKey: string,
193
+ options: ProbeOptions = {},
194
+ ): Promise<LiveValidationResult> {
195
+ return checkBearerGet(
196
+ "https://api.deepseek.com/models",
197
+ {
198
+ headers: { Authorization: `Bearer ${apiKey}` },
199
+ },
200
+ options,
201
+ );
202
+ }
203
+
204
+ /**
205
+ * `GET <baseUrl>/models` with a Bearer key against an OpenAI-compatible
206
+ * registry provider (NaN, Helmcode). Lists models, runs no inference.
207
+ * `baseUrl` defaults to the registry entry; the probe passes the
208
+ * `<ID>_BASE_URL` override, and tests inject a fake server.
209
+ */
210
+ export async function checkOpenAICompatibleProvider(
211
+ provider: OpenAICompatibleProvider,
212
+ apiKey: string,
213
+ baseUrl: string = provider.baseUrl,
214
+ options: ProbeOptions = {},
215
+ ): Promise<LiveValidationResult> {
216
+ const r = await timedFetch(
217
+ `${baseUrl.replace(/\/+$/, "")}/models`,
218
+ {
219
+ method: "GET",
220
+ headers: { Authorization: `Bearer ${apiKey}` },
221
+ },
222
+ options,
223
+ );
224
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
225
+ return {
226
+ ok: false,
227
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
228
+ httpStatus: r.status,
229
+ latency_ms: r.latency_ms,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Run the route provider's own check. Requests stay on the route's origin and
235
+ * path prefix, go to public hosts only, and never follow a redirect.
236
+ */
237
+ async function checkRoute(
238
+ route: ModelRoute,
239
+ env: Record<string, string | undefined>,
240
+ ): Promise<LiveValidationResult> {
241
+ const startedAt = Date.now();
242
+ const provider = getRouteProvider(route.provider);
243
+ const baseUrl = route.baseUrl ?? provider?.defaultBaseUrl;
244
+ if (!provider?.validate || !baseUrl) return { ok: true, skipped: true, latency_ms: 0 };
245
+ const publicFetch = async (url: string | URL | Request, init?: RequestInit) => {
246
+ assertUrlSafe(String(url), publicEndpointSsrfOptions());
247
+ const res = await fetch(url, init);
248
+ if (res.ok || !res.body) return res;
249
+ return new Response(scrubProbeBody(await res.text(), init ?? {}), {
250
+ status: res.status,
251
+ statusText: res.statusText,
252
+ headers: res.headers,
253
+ });
254
+ };
255
+ const result = await provider.validate({
256
+ route,
257
+ env,
258
+ signal: AbortSignal.timeout(LIVE_TEST_TIMEOUT_MS),
259
+ fetch: createScopedFetch(baseUrl, publicFetch as typeof fetch),
260
+ });
261
+ const latency_ms = Date.now() - startedAt;
262
+ if (result.status === "verified") return { ok: true, latency_ms };
263
+ return {
264
+ ok: result.status === "configured",
265
+ ...(result.status === "configured" ? { skipped: true } : { httpStatus: result.httpStatus }),
266
+ error: scrubSecrets(result.reason),
267
+ latency_ms,
268
+ };
269
+ }
270
+
271
+ /** Check one newly submitted pool member without loading a worker harness. */
272
+ export async function checkProviderPoolKey(
273
+ keyType: string,
274
+ apiKey: string,
275
+ env: Record<string, string | undefined> = process.env,
276
+ ): Promise<LiveValidationResult> {
277
+ const startedAt = Date.now();
278
+ const options: ProbeOptions = { env, publicEndpoint: true };
279
+ try {
280
+ switch (keyType) {
281
+ case "ANTHROPIC_API_KEY": {
282
+ // Behind a gateway (`ANTHROPIC_BASE_URL`), Claude Code sends this key
283
+ // to the gateway, so check it there and not at api.anthropic.com.
284
+ const keyEnv = { ...env, ANTHROPIC_API_KEY: apiKey };
285
+ const route = deriveDefaultRoute("claude", keyEnv);
286
+ if (
287
+ route?.provider === "anthropic-gateway" &&
288
+ "secretKey" in route.auth &&
289
+ route.auth.secretKey === "ANTHROPIC_API_KEY"
290
+ ) {
291
+ return await checkRoute(route, keyEnv);
292
+ }
293
+ return await checkAnthropicApiKey(apiKey, options);
294
+ }
295
+ case "OPENAI_API_KEY":
296
+ return await checkOpenAiApiKey(apiKey, options);
297
+ case "OPENROUTER_API_KEY":
298
+ return await checkOpenRouter(apiKey, options);
299
+ case "DEEPSEEK_API_KEY":
300
+ return await checkDeepSeekApiKey(apiKey, options);
301
+ case "XAI_API_KEY":
302
+ return await checkXaiApiKey(apiKey, options);
303
+ case "CLAUDE_CODE_OAUTH_TOKEN": {
304
+ const tokenEnv = { CLAUDE_CODE_OAUTH_TOKEN: apiKey };
305
+ return await checkRoute(deriveDefaultRoute("claude", tokenEnv)!, tokenEnv);
306
+ }
307
+ default: {
308
+ const provider = providerForKeyVar(keyType);
309
+ if (provider) {
310
+ return await checkOpenAICompatibleProvider(
311
+ provider,
312
+ apiKey,
313
+ providerBaseUrl(provider, env),
314
+ options,
315
+ );
316
+ }
317
+ return { ok: true, skipped: true, latency_ms: 0 };
318
+ }
319
+ }
320
+ } catch (err) {
321
+ const raw = err instanceof Error ? err.message : String(err);
322
+ return {
323
+ ok: false,
324
+ error: scrubSecrets(raw.split(apiKey).join("[REDACTED]")) || "Unknown error",
325
+ latency_ms: Date.now() - startedAt,
326
+ };
327
+ }
328
+ }
@@ -20,6 +20,7 @@
20
20
  * auto-detected key always goes to its provider's own endpoint. Deepgram and AssemblyAI use other request shapes and are not
21
21
  * supported. Read at call time, so a config reload applies without a restart.
22
22
  */
23
+ import { selectCredential } from "./credentials";
23
24
  import { createValueScrubber, scrubSecrets } from "./secret-scrubber";
24
25
 
25
26
  const TRANSCRIPTION_TIMEOUT_MS = 60_000;
@@ -119,7 +120,9 @@ export function resolveTranscriptionConfig(): TranscriptionConfig | null {
119
120
  : AUTO_DETECT_ORDER;
120
121
  for (const name of candidates) {
121
122
  const { keyEnv } = PROVIDERS[name];
122
- const apiKey = envValue(keyEnv);
123
+ const value = envValue(keyEnv);
124
+ // OPENAI_API_KEY may be a comma pool: send one member.
125
+ const apiKey = value ? selectCredential(value, undefined, keyEnv).selected : undefined;
123
126
  if (apiKey) return { provider: name, apiKey, keyEnv, source: "auto" };
124
127
  }
125
128
  return null;
@@ -4,16 +4,15 @@ import {
4
4
  getApprovalRequestById,
5
5
  getDbClient,
6
6
  getTaskById,
7
- getWorkflowRun,
8
- listCancelledApprovalRequestsForRun,
9
7
  resolveApprovalRequest,
10
8
  } from "../be/db";
9
+ import { getWorkflowRunStatus } from "../be/db/workflow-runs";
11
10
  import { can, type RbacPrincipal } from "../rbac";
12
11
  import {
13
12
  createApprovalFollowUpTask,
14
13
  postApprovalCancellationUpdates,
15
14
  } from "./approval-notifications";
16
- import { cancelWorkflowRunRows } from "./resume";
15
+ import { cancelWorkflowRunRows, postTreeApprovalCancellations } from "./resume";
17
16
 
18
17
  export type CancelApprovalRequestResult =
19
18
  | { ok: true; request: ApprovalRequest; alreadyCancelled: boolean; runCancelled: boolean }
@@ -126,25 +125,28 @@ export async function cancelApprovalRequestAndRun(
126
125
  if (!request) return null;
127
126
 
128
127
  // The request is cancelled first so it keeps this reason; the run cancel
129
- // then skips it because it is no longer pending.
128
+ // then skips it because it is no longer pending. The run's child tree is
129
+ // cancelled with it.
130
130
  let runCancelled = false;
131
+ let treeRunIds: string[] = [];
131
132
  const runId = request.workflowRunId;
132
133
  if (runId) {
133
- const run = await getWorkflowRun(runId);
134
- if (run && LIVE_RUN_STATUSES.has(run.status)) {
135
- runCancelled = await cancelWorkflowRunRows(runId, data.reason);
134
+ const status = await getWorkflowRunStatus(runId);
135
+ if (status && LIVE_RUN_STATUSES.has(status)) {
136
+ const result = await cancelWorkflowRunRows(runId, data.reason);
137
+ runCancelled = result.cancelled;
138
+ treeRunIds = result.runIds;
136
139
  }
137
140
  }
138
141
 
139
142
  const slackReason = data.slackReason ?? data.reason;
140
143
  client.afterCommit(async () => {
141
144
  await postApprovalCancellationUpdates([request], slackReason);
142
- if (runCancelled && runId) {
143
- // Other requests the run cancel closed get the run's reason.
144
- const others = (await listCancelledApprovalRequestsForRun(runId)).filter(
145
- (row) => row.id !== request.id,
146
- );
147
- await postApprovalCancellationUpdates(others, data.reason);
145
+ if (runCancelled) {
146
+ // Other requests the tree cancel closed get the run's reason.
147
+ await postTreeApprovalCancellations(treeRunIds, data.reason, {
148
+ exceptRequestId: request.id,
149
+ });
148
150
  }
149
151
  });
150
152
  return { request, runCancelled };
@@ -9,6 +9,7 @@ import {
9
9
  releaseApprovalCancellationNotificationClaim,
10
10
  updateApprovalRequestNotifications,
11
11
  } from "../be/db";
12
+ import { approvalCancellationNotificationKey } from "../be/db/approvals";
12
13
  import { resolveTemplate } from "../prompts/resolver";
13
14
  import { getSlackApp } from "../slack/app";
14
15
  import { resolveSlackConversationId } from "../slack/dm";
@@ -263,7 +264,10 @@ export async function postApprovalCancellationUpdates(
263
264
 
264
265
  for (const notification of slackThreads) {
265
266
  if (notification.channel !== "slack" || !notification.messageTs) continue;
266
- const notificationKey = `${notification.target}:${notification.messageTs}`;
267
+ const notificationKey = approvalCancellationNotificationKey(
268
+ notification.target,
269
+ notification.messageTs,
270
+ );
267
271
  const claim = await claimApprovalCancellationNotification(approval.id, notificationKey);
268
272
  if (!claim) continue;
269
273
  try {
@@ -1,12 +1,14 @@
1
1
  import { getDbClient, getWorkflowRun, updateWorkflowRun, updateWorkflowRunStep } from "../be/db";
2
+ import { getRunLineageBlock } from "../be/db/workflow-runs";
2
3
  import type { RetryPolicy } from "../types";
3
4
 
4
5
  /**
5
6
  * The execution a checkpoint belongs to: the step attempt (`retryCount`) that
6
7
  * claimed the row. A fenced checkpoint writes only while that attempt still
7
8
  * owns the row: the step is `running` with that `retryCount` in a `running` or
8
- * `waiting` run. A cancel, a newer attempt or another settle that committed
9
- * meanwhile wins, and the stale execution writes nothing.
9
+ * `waiting` run with no cancelled ancestor. A cancel, a newer attempt or
10
+ * another settle that committed meanwhile wins, and the stale execution
11
+ * writes nothing.
10
12
  */
11
13
  export type StepFence = { retryCount: number };
12
14
 
@@ -19,7 +21,7 @@ async function holdsFence(runId: string, stepId: string, fence: StepFence): Prom
19
21
  AND r.status IN ('running', 'waiting')`,
20
22
  [stepId, runId, fence.retryCount],
21
23
  );
22
- return row != null;
24
+ return row != null && (await getRunLineageBlock(runId)) == null;
23
25
  }
24
26
 
25
27
  /**
@@ -129,6 +131,7 @@ export async function checkpointStepWaiting(
129
131
  options: { keepRunStatus?: boolean } = {},
130
132
  ): Promise<boolean> {
131
133
  return getDbClient().transaction(async () => {
134
+ if (await getRunLineageBlock(runId)) return false;
132
135
  const step = await getDbClient().get<{ id: string }>(
133
136
  `UPDATE workflow_run_steps
134
137
  SET status = 'waiting'
@@ -21,7 +21,11 @@ import {
21
21
  updateWorkflowRun,
22
22
  updateWorkflowRunStep,
23
23
  } from "../be/db";
24
- import { getChildWorkflowRunId, isParentStepAttemptCurrent } from "../be/db/workflow-runs";
24
+ import {
25
+ getChildWorkflowRunId,
26
+ getRunLineageBlock,
27
+ isParentStepAttemptCurrent,
28
+ } from "../be/db/workflow-runs";
25
29
  import { WorkflowReplayStateError } from "../be/workflow-replay";
26
30
  import { telemetry } from "../telemetry";
27
31
  import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
@@ -67,6 +71,13 @@ export interface WorkflowExecutionOptions {
67
71
  parentStepId?: string;
68
72
  /** The parent step's `retryCount`; each attempt gets its own child run. */
69
73
  parentStepAttempt?: number;
74
+ /**
75
+ * Runs inside the transaction that commits a top-level run (or reuses the
76
+ * day's setup-failure run), after the run row is written. A throw rolls the
77
+ * run back and nothing else happens: no walk, no alert, no telemetry. The
78
+ * HTTP trigger records its `Idempotency-Key` here.
79
+ */
80
+ accept?: (run: { runId: string; skipped: boolean }) => Promise<void>;
70
81
  }
71
82
 
72
83
  /**
@@ -111,12 +122,16 @@ export async function startWorkflowExecution(
111
122
  await getAutomationSetupStates(),
112
123
  );
113
124
  if (preflight.state === "needs_setup" && !options.parentStepId) {
114
- const { runId, recorded } = await recordWorkflowPreflightFailure({
115
- workflowId: workflow.id,
116
- triggerType: options.triggerType ?? "manual",
117
- triggerData,
118
- failureReason: preflight.failureReason!,
119
- createdBy: options.requestedByUserId,
125
+ const { runId, recorded } = await getDbClient().transaction(async () => {
126
+ const failure = await recordWorkflowPreflightFailure({
127
+ workflowId: workflow.id,
128
+ triggerType: options.triggerType ?? "manual",
129
+ triggerData,
130
+ failureReason: preflight.failureReason!,
131
+ createdBy: options.requestedByUserId,
132
+ });
133
+ await options.accept?.({ runId: failure.runId, skipped: false });
134
+ return failure;
120
135
  });
121
136
  if (recorded) await notifyAutomationPreflightFailure(preflight);
122
137
  return runId;
@@ -276,7 +291,8 @@ async function buildInitialRunState(
276
291
  * executing and returns the child that attempt already started instead of a
277
292
  * second one; the unique (parentStepId, parentStepAttempt) index backs this
278
293
  * across processes. A setup failure claims the day's alert in the same
279
- * transaction, so concurrent failures alert exactly once.
294
+ * transaction, so concurrent failures alert exactly once. `options.accept`
295
+ * runs last in it.
280
296
  */
281
297
  async function createRunWithInitialState(
282
298
  workflow: Workflow,
@@ -314,6 +330,7 @@ async function createRunWithInitialState(
314
330
  initial.status === "failed" && initial.preflight
315
331
  ? await isFirstWorkflowPreflightFailureToday(workflow.id, runId)
316
332
  : undefined;
333
+ await options.accept?.({ runId, skipped: initial.status === "skipped" });
317
334
  return { runId, created: true, preflightAlert };
318
335
  });
319
336
  }
@@ -606,6 +623,8 @@ async function walkGraphOwned(
606
623
  await getDbClient().transaction(async () => {
607
624
  const run = await getWorkflowRun(runId);
608
625
  if (!run || run.status !== "running") return;
626
+ // Inside a cancelled tree the run's outcome is the cancel, not its steps.
627
+ if (await getRunLineageBlock(runId)) return;
609
628
 
610
629
  const finalSteps = await getWorkflowRunStepsByRunId(runId);
611
630
  // Another walker is still executing a branch; it finalizes the run.
@@ -793,6 +812,9 @@ async function executeStep(
793
812
  if (!run || (run.status !== "running" && run.status !== "waiting")) {
794
813
  return { halted: true as const };
795
814
  }
815
+ // A live run under a cancelled ancestor (or with a broken parent link)
816
+ // starts no step: the cancel covers its whole tree.
817
+ if (await getRunLineageBlock(runId)) return { halted: true as const };
796
818
 
797
819
  // Another walker already started or finished this node for the same
798
820
  // predecessor completions (two branch completions both walking the join,
@@ -6,7 +6,7 @@ import { getSuccessors } from "../definition";
6
6
  import { foreachBodyTypeError } from "../foreach-body";
7
7
  import {
8
8
  buildForeachAggregate,
9
- FOREACH_TERMINAL_STEP_STATUSES,
9
+ isForeachChildSettled,
10
10
  parseSyntheticNodeId,
11
11
  } from "../foreach-join";
12
12
  import { getMaxWorkflowStepsPerRun } from "../limits";
@@ -144,7 +144,7 @@ export class ForeachExecutor extends BaseExecutor<
144
144
  const childSteps = (await this.deps.db.getWorkflowRunStepsByRunId(meta.runId)).filter(
145
145
  (step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId,
146
146
  );
147
- if (childSteps.every((step) => FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
147
+ if (childSteps.every(isForeachChildSettled)) {
148
148
  return { status: "success", output: buildForeachAggregate(childSteps) };
149
149
  }
150
150
 
@@ -133,7 +133,14 @@ export class SubWorkflowExecutor extends BaseExecutor<
133
133
  throw err;
134
134
  }
135
135
  const child = await db.getWorkflowRun(childRunId);
136
- if (!child) return { status: "failed", error: `Child workflow run ${childRunId} not found` };
136
+ // The start committed the row, so a missing one was deleted with its workflow.
137
+ if (!child) {
138
+ return {
139
+ status: "failed",
140
+ error: `Child workflow run ${childRunId} not found`,
141
+ retryable: false,
142
+ };
143
+ }
137
144
  return this.settle(child, config.workflowId);
138
145
  }
139
146
 
@@ -11,6 +11,7 @@ import {
11
11
  updateWorkflowRun,
12
12
  updateWorkflowRunStep,
13
13
  } from "../be/db";
14
+ import { getRunLineageBlock, getWorkflowRunStatus } from "../be/db/workflow-runs";
14
15
  import type {
15
16
  Workflow,
16
17
  WorkflowDefinition,
@@ -41,6 +42,7 @@ import {
41
42
  cancelQueuedForeachChildren,
42
43
  FOREACH_TERMINAL_STEP_STATUSES,
43
44
  type ForeachChildOutcome,
45
+ isRetryPending,
44
46
  latestForeachChildren,
45
47
  settleForeachChild,
46
48
  } from "./foreach-join";
@@ -352,8 +354,8 @@ async function parkRunIfRunning(runId: string): Promise<void> {
352
354
 
353
355
  /**
354
356
  * 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
+ * fan-out's `concurrency` cap are in flight (`running`, `waiting` or queued
358
+ * for a retry, latest attempt rows only), and collect unowned `running` ones. Ownership is registered before commit, so no other
357
359
  * drive can see a claimed row without also seeing it owned.
358
360
  */
359
361
  async function claimChildren(
@@ -367,6 +369,10 @@ async function claimChildren(
367
369
  // so concurrent drives (a settlement's drive and the recovery sweep)
368
370
  // serialize here and never exceed the cap together.
369
371
  await getDbClient().transaction(async () => {
372
+ // A cancel of the run (or of an ancestor) that committed first wins.
373
+ const status = await getWorkflowRunStatus(runId);
374
+ if (status !== "running" && status !== "waiting") return;
375
+ if (await getRunLineageBlock(runId)) return;
370
376
  const children = latestForeachChildren(
371
377
  def,
372
378
  parentNode.id,
@@ -379,7 +385,8 @@ async function claimChildren(
379
385
  }
380
386
  }
381
387
  const inFlight = children.filter(
382
- (child) => child.status === "running" || child.status === "waiting",
388
+ (child) =>
389
+ child.status === "running" || child.status === "waiting" || isRetryPending(child),
383
390
  ).length;
384
391
  let free = foreachConcurrencyCap(parentNode) - inFlight;
385
392
  for (const child of children) {