@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
@@ -1,12 +1,161 @@
1
- import type { WorkflowRunStatus } from "../../types";
1
+ import {
2
+ TERMINAL_TASK_STATUSES,
3
+ type WorkflowRun,
4
+ type WorkflowRunStatus,
5
+ type WorkflowRunStep,
6
+ type WorkflowRunStepSlim,
7
+ type WorkflowRunStepStatus,
8
+ type WorkflowRunSummary,
9
+ } from "../../types";
2
10
  import { scrubSecrets } from "../../utils/secret-scrubber";
11
+ import { normalizeDate, normalizeDateRequired } from "../date-utils";
3
12
  import { scrubJsonValue } from "../scrub-json";
13
+ import { defineWorkflowPayload, type WorkflowPayloadView } from "../workflow-replay";
4
14
  import { getDbClient } from "./runtime";
5
15
 
6
16
  // Parent/child links between workflow runs (migration 203): a `sub-workflow`
7
17
  // step starts a child run that records the step in `parentStepId`, and the
8
18
  // step's `retryCount` at that time in `parentStepAttempt` (migration 240).
9
19
 
20
+ export type WorkflowRunRow = {
21
+ id: string;
22
+ workflowId: string;
23
+ status: string;
24
+ triggerType: string;
25
+ triggerData: string | null;
26
+ /** Absent from list rows: see `WORKFLOW_RUN_SUMMARY_COLUMNS`. */
27
+ context?: string | null;
28
+ /** Sealed exact `context` (migration 201). Absent from list rows. */
29
+ context_replay?: string | null;
30
+ error: string | null;
31
+ created_by: string | null;
32
+ startedAt: string;
33
+ lastUpdatedAt: string;
34
+ finishedAt: string | null;
35
+ /** The `sub-workflow` step that started this run (migration 203). */
36
+ parentStepId?: string | null;
37
+ /** That step's attempt (`retryCount`) when it started this run (migration 240). */
38
+ parentStepAttempt?: number | null;
39
+ };
40
+
41
+ /**
42
+ * `view` picks the `context` copy: `replay` (default) is the exact sealed value
43
+ * the engine resumes from; `display` is the scrubbed column for API, MCP and UI
44
+ * responses. See `src/be/workflow-replay.ts`.
45
+ */
46
+ export function rowToWorkflowRun(
47
+ row: WorkflowRunRow,
48
+ view: WorkflowPayloadView = "replay",
49
+ ): WorkflowRun {
50
+ const run: WorkflowRun = {
51
+ id: row.id,
52
+ workflowId: row.workflowId,
53
+ status: row.status as WorkflowRunStatus,
54
+ triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
55
+ context: undefined,
56
+ error: row.error ?? undefined,
57
+ createdBy: row.created_by ?? undefined,
58
+ startedAt: normalizeDateRequired(row.startedAt),
59
+ lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
60
+ finishedAt: normalizeDate(row.finishedAt) ?? undefined,
61
+ parentStepId: row.parentStepId ?? undefined,
62
+ parentStepAttempt: row.parentStepId ? (row.parentStepAttempt ?? 0) : undefined,
63
+ };
64
+ return defineWorkflowPayload(run, "context", row.context, row.context_replay, view, {
65
+ runId: row.id,
66
+ target: "its context",
67
+ });
68
+ }
69
+
70
+ // Everything but `context`. `triggerData` stays: it is small, and stored
71
+ // scripts read it off list rows (dedupe by trigger, failure reports). The
72
+ // parent link and attempt stay so a list row identifies a child's invocation.
73
+ export const WORKFLOW_RUN_SUMMARY_COLUMNS =
74
+ "id, workflowId, status, triggerType, triggerData, error, created_by, startedAt, lastUpdatedAt, finishedAt, parentStepId, parentStepAttempt";
75
+
76
+ export type WorkflowRunStepRow = {
77
+ id: string;
78
+ runId: string;
79
+ nodeId: string;
80
+ nodeType: string;
81
+ status: string;
82
+ /** Absent from slim rows: see `WORKFLOW_RUN_STEP_SLIM_COLUMNS`. */
83
+ input?: string | null;
84
+ output: string | null;
85
+ /** Sealed exact `output` (migration 201). Absent from slim rows. */
86
+ output_replay?: string | null;
87
+ error: string | null;
88
+ startedAt: string;
89
+ finishedAt: string | null;
90
+ retryCount: number;
91
+ maxRetries: number;
92
+ nextRetryAt: string | null;
93
+ idempotencyKey: string | null;
94
+ diagnostics: string | null;
95
+ nextPort: string | null;
96
+ };
97
+
98
+ /** `view` picks the `output` copy, as in `rowToWorkflowRun`. */
99
+ export function rowToWorkflowRunStep(
100
+ row: WorkflowRunStepRow,
101
+ view: WorkflowPayloadView = "replay",
102
+ ): WorkflowRunStep {
103
+ const step: WorkflowRunStep = {
104
+ id: row.id,
105
+ runId: row.runId,
106
+ nodeId: row.nodeId,
107
+ nodeType: row.nodeType,
108
+ status: row.status as WorkflowRunStepStatus,
109
+ input: row.input ? JSON.parse(row.input) : undefined,
110
+ output: undefined,
111
+ error: row.error ?? undefined,
112
+ startedAt: normalizeDateRequired(row.startedAt),
113
+ finishedAt: normalizeDate(row.finishedAt) ?? undefined,
114
+ retryCount: row.retryCount,
115
+ maxRetries: row.maxRetries,
116
+ nextRetryAt: normalizeDate(row.nextRetryAt) ?? undefined,
117
+ idempotencyKey: row.idempotencyKey ?? undefined,
118
+ diagnostics: row.diagnostics ?? undefined,
119
+ nextPort: row.nextPort ?? undefined,
120
+ };
121
+ return defineWorkflowPayload(step, "output", row.output, row.output_replay, view, {
122
+ runId: row.runId,
123
+ target: `the output of step ${row.id} (node ${row.nodeId})`,
124
+ });
125
+ }
126
+
127
+ // Everything but `input` (a context snapshot per step, so loops grow it with
128
+ // every iteration) and `output_replay` (the display view never opens it).
129
+ const WORKFLOW_RUN_STEP_SLIM_COLUMNS =
130
+ "id, runId, nodeId, nodeType, status, output, error, startedAt, finishedAt, retryCount, maxRetries, nextRetryAt, idempotencyKey, diagnostics, nextPort";
131
+
132
+ /**
133
+ * `getWorkflowRun` without `context`, in the display view. Backs
134
+ * `GET /api/workflow-runs/{id}?fields=slim`: the large column is neither read
135
+ * nor parsed.
136
+ */
137
+ export async function getWorkflowRunSummary(id: string): Promise<WorkflowRunSummary | null> {
138
+ const row = await getDbClient().get<WorkflowRunRow>(
139
+ `SELECT ${WORKFLOW_RUN_SUMMARY_COLUMNS} FROM workflow_runs WHERE id = ?`,
140
+ [id],
141
+ );
142
+ return row ? rowToWorkflowRun(row, "display") : null;
143
+ }
144
+
145
+ /**
146
+ * `getWorkflowRunStepsByRunId` without `input`, in the display view. Same
147
+ * order, including the rowid tie-break for steps that share `startedAt`.
148
+ */
149
+ export async function getWorkflowRunStepsSlimByRunId(
150
+ runId: string,
151
+ ): Promise<WorkflowRunStepSlim[]> {
152
+ const rows = await getDbClient().query<WorkflowRunStepRow>(
153
+ `SELECT ${WORKFLOW_RUN_STEP_SLIM_COLUMNS} FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC, rowid ASC`,
154
+ [runId],
155
+ );
156
+ return rows.map((row) => rowToWorkflowRunStep(row, "display"));
157
+ }
158
+
10
159
  const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
11
160
  "completed",
12
161
  "failed",
@@ -14,6 +163,325 @@ const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
14
163
  "skipped",
15
164
  ]);
16
165
 
166
+ /**
167
+ * Deepest run tree the lineage guard and the cascade cancel walk. Twice the
168
+ * sub-workflow nesting cap (MAX_SUB_WORKFLOW_DEPTH, 16), so any tree the
169
+ * engine admits fits; a deeper chain is corrupt and fails closed.
170
+ */
171
+ export const WORKFLOW_RUN_TREE_MAX_DEPTH = 32;
172
+
173
+ /** Thrown when a run's stored parent links cannot be walked: a cycle, or a chain past the depth bound. */
174
+ export class WorkflowRunTreeError extends Error {
175
+ constructor(message: string) {
176
+ super(message);
177
+ this.name = "WorkflowRunTreeError";
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Thrown when work is created for a workflow step that may no longer run: the
183
+ * step is not `running`, its run is not live, or the run's lineage is blocked
184
+ * (see `getRunLineageBlock`).
185
+ */
186
+ export class WorkflowStepNotExecutableError extends Error {
187
+ constructor(stepId: string, reason: string) {
188
+ super(`Workflow step ${stepId} may not start new work: ${reason}`);
189
+ this.name = "WorkflowStepNotExecutableError";
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Why the run and its ancestors forbid new engine work, or null when they
195
+ * allow it. Blocked when the run is missing, the run or an ancestor is
196
+ * `cancelled`, a stored parent link points at a missing step or run, the
197
+ * links form a cycle, or the chain is deeper than WORKFLOW_RUN_TREE_MAX_DEPTH.
198
+ * A completed or failed ancestor does not block: a child of an earlier
199
+ * attempt may outlive its parent's run. The run's own live status is left to
200
+ * the caller, since the retry claim also revives a `failed` run.
201
+ *
202
+ * Call it inside the transaction that writes the new work, so a cascade
203
+ * cancel either commits first and is seen here or sees the new row itself.
204
+ */
205
+ export async function getRunLineageBlock(runId: string): Promise<string | null> {
206
+ const rows = await getDbClient().query<{
207
+ id: string;
208
+ status: WorkflowRunStatus;
209
+ parentStepId: string | null;
210
+ depth: number;
211
+ }>(
212
+ `WITH RECURSIVE lineage(id, status, parentStepId, depth) AS (
213
+ SELECT id, status, parentStepId, 0 FROM workflow_runs WHERE id = ?
214
+ UNION ALL
215
+ SELECT r.id, r.status, r.parentStepId, l.depth + 1
216
+ FROM lineage l
217
+ JOIN workflow_run_steps s ON s.id = l.parentStepId
218
+ JOIN workflow_runs r ON r.id = s.runId
219
+ WHERE l.depth < ?
220
+ )
221
+ SELECT id, status, parentStepId, depth FROM lineage ORDER BY depth`,
222
+ [runId, WORKFLOW_RUN_TREE_MAX_DEPTH],
223
+ );
224
+ if (rows.length === 0) return `Workflow run ${runId} is missing`;
225
+ const seen = new Set<string>();
226
+ for (const row of rows) {
227
+ if (seen.has(row.id)) return `Workflow run ${runId} has a cycle in its parent links`;
228
+ seen.add(row.id);
229
+ if (row.status === "cancelled") {
230
+ return row.id === runId
231
+ ? `Workflow run ${runId} is cancelled`
232
+ : `Ancestor workflow run ${row.id} is cancelled`;
233
+ }
234
+ }
235
+ const top = rows[rows.length - 1]!;
236
+ if (top.parentStepId == null) return null;
237
+ if (top.depth >= WORKFLOW_RUN_TREE_MAX_DEPTH) {
238
+ return `Workflow run ${runId} is nested deeper than ${WORKFLOW_RUN_TREE_MAX_DEPTH} levels`;
239
+ }
240
+ return `Parent step ${top.parentStepId} of workflow run ${top.id} is missing`;
241
+ }
242
+
243
+ /**
244
+ * The run and every run below it, root first: children of every attempt of
245
+ * every step, through terminal runs too, since a run that ended can still have
246
+ * a live child of an earlier attempt. The tree comes from stored links only
247
+ * (child.parentStepId → step → run). There is no size limit: the engine admits
248
+ * any number of children per run, and a tree it admitted must stay
249
+ * cancellable. Throws WorkflowRunTreeError on a cycle or past `maxDepth`, so
250
+ * the caller's transaction rolls back whole.
251
+ */
252
+ export async function getWorkflowRunTreeIds(
253
+ rootId: string,
254
+ bounds: { maxDepth?: number } = {},
255
+ ): Promise<string[]> {
256
+ const maxDepth = bounds.maxDepth ?? WORKFLOW_RUN_TREE_MAX_DEPTH;
257
+ const ids = [rootId];
258
+ const seen = new Set(ids);
259
+ // One level per pass, so a cycle stops at its first repeated run instead of
260
+ // unrolling down to the depth bound.
261
+ let level = [rootId];
262
+ for (let depth = 1; level.length > 0; depth++) {
263
+ const next: string[] = [];
264
+ for (let i = 0; i < level.length; i += TREE_QUERY_CHUNK) {
265
+ const chunk = level.slice(i, i + TREE_QUERY_CHUNK);
266
+ const rows = await getDbClient().query<{ id: string }>(
267
+ `SELECT c.id
268
+ FROM workflow_run_steps s
269
+ JOIN workflow_runs c ON c.parentStepId = s.id
270
+ WHERE s.runId IN (${chunk.map(() => "?").join(", ")})`,
271
+ chunk,
272
+ );
273
+ for (const { id } of rows) {
274
+ // Each run has one parent link, so reaching a run twice means a cycle.
275
+ if (seen.has(id)) {
276
+ throw new WorkflowRunTreeError(`Workflow run tree of ${rootId} has a cycle at run ${id}`);
277
+ }
278
+ seen.add(id);
279
+ next.push(id);
280
+ }
281
+ }
282
+ if (next.length > 0 && depth > maxDepth) {
283
+ throw new WorkflowRunTreeError(
284
+ `Workflow run tree of ${rootId} is nested deeper than ${maxDepth} levels`,
285
+ );
286
+ }
287
+ ids.push(...next);
288
+ level = next;
289
+ }
290
+ return ids;
291
+ }
292
+
293
+ /** Parent runs per children query, well under SQLite's bound-parameter limit. */
294
+ const TREE_QUERY_CHUNK = 500;
295
+
296
+ /** A run's status alone, without opening its sealed context. Null when the run is missing. */
297
+ export async function getWorkflowRunStatus(runId: string): Promise<WorkflowRunStatus | null> {
298
+ const row = await getDbClient().get<{ status: WorkflowRunStatus }>(
299
+ "SELECT status FROM workflow_runs WHERE id = ?",
300
+ [runId],
301
+ );
302
+ return row?.status ?? null;
303
+ }
304
+
305
+ /** Run `fn` over `ids` in chunks small enough for one statement's bound parameters. */
306
+ async function forEachChunk(
307
+ ids: string[],
308
+ fn: (chunk: string[], placeholders: string) => Promise<void>,
309
+ ): Promise<void> {
310
+ for (let i = 0; i < ids.length; i += TREE_QUERY_CHUNK) {
311
+ const chunk = ids.slice(i, i + TREE_QUERY_CHUNK);
312
+ await fn(chunk, chunk.map(() => "?").join(", "));
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Ids of the non-terminal tasks of the given runs, including tasks of earlier
318
+ * attempts: a retry detaches a task from its step, but the run link stays.
319
+ */
320
+ export async function getOpenWorkflowTreeTaskIds(runIds: string[]): Promise<string[]> {
321
+ const ids: string[] = [];
322
+ const terminal = TERMINAL_TASK_STATUSES.map(() => "?").join(", ");
323
+ await forEachChunk(runIds, async (chunk, placeholders) => {
324
+ const rows = await getDbClient().query<{ id: string }>(
325
+ `SELECT id FROM agent_tasks
326
+ WHERE workflowRunId IN (${placeholders}) AND status NOT IN (${terminal})`,
327
+ [...chunk, ...TERMINAL_TASK_STATUSES],
328
+ );
329
+ for (const row of rows) ids.push(row.id);
330
+ });
331
+ return ids;
332
+ }
333
+
334
+ /**
335
+ * Ids of tasks that a non-terminal task lists in `dependsOn`: the only
336
+ * dependents a cancel can fail. One scan over open tasks, so a cancel of many
337
+ * tasks looks for dependents once instead of once per task.
338
+ */
339
+ export async function getTaskIdsWithDependents(): Promise<Set<string>> {
340
+ const rows = await getDbClient().query<{ id: string }>(
341
+ `SELECT DISTINCT dep.value AS id
342
+ FROM agent_tasks t, json_each(t.dependsOn) AS dep
343
+ WHERE t.status NOT IN (${TERMINAL_TASK_STATUSES.map(() => "?").join(", ")})
344
+ AND t.dependsOn IS NOT NULL AND t.dependsOn != '[]'`,
345
+ [...TERMINAL_TASK_STATUSES],
346
+ );
347
+ return new Set(rows.map((row) => row.id));
348
+ }
349
+
350
+ /**
351
+ * True when any of the given runs still has work a cancel would close: a live
352
+ * run, a pending, running or waiting step, a queued retry, an open task or a
353
+ * pending approval. Reads only, with a LIMIT 1 probe per chunk.
354
+ */
355
+ export async function workflowRunTreeHasOpenWork(runIds: string[]): Promise<boolean> {
356
+ const terminal = TERMINAL_TASK_STATUSES.map(() => "?").join(", ");
357
+ const probes: Array<{ sql: (ids: string) => string; params: string[] }> = [
358
+ {
359
+ sql: (ids) =>
360
+ `SELECT 1 FROM workflow_runs WHERE id IN (${ids}) AND status IN ('running', 'waiting') LIMIT 1`,
361
+ params: [],
362
+ },
363
+ {
364
+ sql: (ids) => `SELECT 1 FROM workflow_run_steps
365
+ WHERE runId IN (${ids})
366
+ AND (status IN ('pending', 'running', 'waiting')
367
+ OR (status = 'failed' AND nextRetryAt IS NOT NULL))
368
+ LIMIT 1`,
369
+ params: [],
370
+ },
371
+ {
372
+ sql: (ids) =>
373
+ `SELECT 1 FROM agent_tasks WHERE workflowRunId IN (${ids}) AND status NOT IN (${terminal}) LIMIT 1`,
374
+ params: [...TERMINAL_TASK_STATUSES],
375
+ },
376
+ {
377
+ sql: (ids) =>
378
+ `SELECT 1 FROM approval_requests WHERE workflowRunId IN (${ids}) AND status = 'pending' LIMIT 1`,
379
+ params: [],
380
+ },
381
+ ];
382
+ let open = false;
383
+ await forEachChunk(runIds, async (chunk, placeholders) => {
384
+ for (const probe of probes) {
385
+ if (open) return;
386
+ if (await getDbClient().get(probe.sql(placeholders), [...chunk, ...probe.params])) {
387
+ open = true;
388
+ }
389
+ }
390
+ });
391
+ return open;
392
+ }
393
+
394
+ export type WorkflowRunTree = {
395
+ rootRunId: string;
396
+ /** Root first, in `getWorkflowRunTreeIds` order. List rows: no `context`. */
397
+ runs: WorkflowRun[];
398
+ hasOpenWork: boolean;
399
+ };
400
+
401
+ /**
402
+ * The run and every run below it, with whether any of them still has open
403
+ * work (see `workflowRunTreeHasOpenWork`). One read transaction, so `runs` and
404
+ * `hasOpenWork` come from the same snapshot. Null when the root is missing.
405
+ * Throws WorkflowRunTreeError on a cycle or a chain past the depth bound.
406
+ */
407
+ export async function getWorkflowRunTree(rootRunId: string): Promise<WorkflowRunTree | null> {
408
+ return getDbClient().transaction(
409
+ async () => {
410
+ if ((await getWorkflowRunStatus(rootRunId)) === null) return null;
411
+ const ids = await getWorkflowRunTreeIds(rootRunId);
412
+ const byId = new Map<string, WorkflowRun>();
413
+ await forEachChunk(ids, async (chunk, placeholders) => {
414
+ const rows = await getDbClient().query<WorkflowRunRow>(
415
+ `SELECT ${WORKFLOW_RUN_SUMMARY_COLUMNS} FROM workflow_runs WHERE id IN (${placeholders})`,
416
+ chunk,
417
+ );
418
+ for (const row of rows) byId.set(row.id, rowToWorkflowRun(row, "display"));
419
+ });
420
+ const runs = ids.flatMap((id) => byId.get(id) ?? []);
421
+ return { rootRunId, runs, hasOpenWork: await workflowRunTreeHasOpenWork(ids) };
422
+ },
423
+ { readOnly: true },
424
+ );
425
+ }
426
+
427
+ /**
428
+ * Cancel the pending, running and waiting steps of the given runs. The
429
+ * migration-140 trigger closes each cancelled step's pending approval.
430
+ */
431
+ export async function cancelOpenWorkflowTreeSteps(
432
+ runIds: string[],
433
+ reason: string,
434
+ finishedAt: string,
435
+ ): Promise<void> {
436
+ await forEachChunk(runIds, async (chunk, placeholders) => {
437
+ await getDbClient().run(
438
+ `UPDATE workflow_run_steps
439
+ SET status = 'cancelled', error = ?, finishedAt = ?
440
+ WHERE runId IN (${placeholders}) AND status IN ('pending', 'running', 'waiting')`,
441
+ [scrubSecrets(reason), finishedAt, ...chunk],
442
+ );
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Drop the pending retries of the failed steps of the given runs. The step
448
+ * keeps its status, error, output and retryCount; only the queued next
449
+ * attempt goes.
450
+ */
451
+ export async function disarmWorkflowTreeRetries(
452
+ runIds: string[],
453
+ finishedAt: string,
454
+ ): Promise<void> {
455
+ await forEachChunk(runIds, async (chunk, placeholders) => {
456
+ await getDbClient().run(
457
+ `UPDATE workflow_run_steps
458
+ SET nextRetryAt = NULL, finishedAt = COALESCE(finishedAt, ?)
459
+ WHERE runId IN (${placeholders}) AND status = 'failed' AND nextRetryAt IS NOT NULL`,
460
+ [finishedAt, ...chunk],
461
+ );
462
+ });
463
+ }
464
+
465
+ /**
466
+ * Mark the given runs that are still running or waiting `cancelled`, without
467
+ * the per-run `workflow.child.finished` wake-up: it is for runs whose parent
468
+ * step the same cancel closes. A terminal run keeps its status and reason.
469
+ */
470
+ export async function cancelLiveWorkflowTreeRuns(
471
+ runIds: string[],
472
+ reason: string,
473
+ finishedAt: string,
474
+ ): Promise<void> {
475
+ await forEachChunk(runIds, async (chunk, placeholders) => {
476
+ await getDbClient().run(
477
+ `UPDATE workflow_runs
478
+ SET status = 'cancelled', error = ?, finishedAt = ?, lastUpdatedAt = ?
479
+ WHERE id IN (${placeholders}) AND status IN ('running', 'waiting')`,
480
+ [scrubSecrets(reason), finishedAt, finishedAt, ...chunk],
481
+ );
482
+ });
483
+ }
484
+
17
485
  /** The id of the child run a `sub-workflow` step started on `attempt`, if any. */
18
486
  export async function getChildWorkflowRunId(
19
487
  parentStepId: string,
@@ -28,23 +496,48 @@ export async function getChildWorkflowRunId(
28
496
 
29
497
  /**
30
498
  * True while `attempt` of the step is executing in a live run: the step is
31
- * `running` with that `retryCount`. A child run is created only then, so a
32
- * cancelled run, a step another attempt moved on, or a step already parked
33
- * never gets a new child.
499
+ * `running` with that `retryCount` and no ancestor run is cancelled. A child
500
+ * run is created only then, so a cancelled run or tree, a step another attempt
501
+ * moved on, or a step already parked never gets a new child.
34
502
  */
35
503
  export async function isParentStepAttemptCurrent(
36
504
  parentStepId: string,
37
505
  attempt: number,
38
506
  ): Promise<boolean> {
39
- const row = await getDbClient().get<{ found: number }>(
40
- `SELECT 1 AS found
507
+ const row = await getDbClient().get<{ runId: string }>(
508
+ `SELECT r.id AS runId
41
509
  FROM workflow_run_steps s
42
510
  JOIN workflow_runs r ON r.id = s.runId
43
511
  WHERE s.id = ? AND s.status = 'running' AND s.retryCount = ?
44
512
  AND r.status IN ('running', 'waiting')`,
45
513
  [parentStepId, attempt],
46
514
  );
47
- return row != null;
515
+ return row != null && (await getRunLineageBlock(row.runId)) == null;
516
+ }
517
+
518
+ /**
519
+ * Why `stepId` may not start new work (a task, an approval, a child run), or
520
+ * null when it may: the step is `running` in a live run whose lineage is not
521
+ * blocked. Call it inside the transaction that inserts the work.
522
+ */
523
+ export async function getStepExecutionBlock(stepId: string): Promise<string | null> {
524
+ const row = await getDbClient().get<{
525
+ stepStatus: string;
526
+ runId: string;
527
+ runStatus: WorkflowRunStatus;
528
+ }>(
529
+ `SELECT s.status AS stepStatus, r.id AS runId, r.status AS runStatus
530
+ FROM workflow_run_steps s
531
+ JOIN workflow_runs r ON r.id = s.runId
532
+ WHERE s.id = ?`,
533
+ [stepId],
534
+ );
535
+ if (!row) return "the step or its run is missing";
536
+ if (row.runStatus !== "running" && row.runStatus !== "waiting") {
537
+ return `its run is ${row.runStatus}`;
538
+ }
539
+ if (row.stepStatus !== "running") return `the step is ${row.stepStatus}`;
540
+ return getRunLineageBlock(row.runId);
48
541
  }
49
542
 
50
543
  /**
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { Attributes, DbRetentionSweepMetric, SpanStatus, SwarmSpan } from "../otel";
3
3
  import { recordDbRetentionStatement, recordDbRetentionSweep, startSpan, withSpan } from "../otel";
4
+ import { TERMINAL_SCRIPT_RUN_STATUSES } from "../types";
4
5
  import { isEnvFlagEnabled } from "../utils/env-flag";
5
6
  import { scrubSecrets } from "../utils/secret-scrubber";
6
7
  import { getDbClient } from "./db";
@@ -23,7 +24,11 @@ const YIELD_MS = 25;
23
24
  *
24
25
  * Two policies exist:
25
26
  * - `age`: the env value is a number of days; rows whose `timeColumn` is older
26
- * than that horizon are deleted.
27
+ * than that horizon are deleted. Optional narrowing: `terminalStatuses`
28
+ * deletes only rows whose `statusColumn` is in that list, `keepWhenSet`
29
+ * keeps every row where that column is non-NULL, and
30
+ * `cutoffFormat: "sqliteDatetime"` compares against a `YYYY-MM-DD HH:MM:SS`
31
+ * cutoff for a column that mixes that format with ISO strings.
27
32
  * - `keepLatest`: the env value is a count; within each `partitionBy` group,
28
33
  * only the newest `rankBy` rows are kept. The newest row of a group is
29
34
  * never deleted, because the smallest accepted count is 1.
@@ -65,6 +70,29 @@ export const DB_RETENTION_TABLES = [
65
70
  initialBatchSize: 50,
66
71
  maxBatchSize: 100,
67
72
  },
73
+ {
74
+ // Every inline script run stores its full source, args and output. Only
75
+ // terminal runs are eligible: a running or paused durable run must survive
76
+ // to be resumed. Runs with an idempotencyKey are kept, so a retried launch
77
+ // still gets its 409 instead of executing twice. The status filter, the
78
+ // time range and the key exclusion all resolve on indexes
79
+ // (idx_script_runs_status_startedAt, idx_script_runs_idempotencyKey).
80
+ // Workflow runs write `datetime('now')` (space separator) while inline runs
81
+ // write ISO strings, hence the sqliteDatetime cutoff. script_run_journal
82
+ // rows go with their run through ON DELETE CASCADE on idx_srj_runId.
83
+ // Rows average ~30 KB and reach ~1.3 MB, so batches stay small.
84
+ table: "script_runs",
85
+ policy: "age",
86
+ timeColumn: "startedAt",
87
+ cutoffFormat: "sqliteDatetime",
88
+ statusColumn: "status",
89
+ terminalStatuses: TERMINAL_SCRIPT_RUN_STATUSES,
90
+ keepWhenSet: "idempotencyKey",
91
+ envKey: "SCRIPT_RUNS_RETENTION_DAYS",
92
+ metricsKey: "scriptRuns",
93
+ initialBatchSize: 100,
94
+ maxBatchSize: 500,
95
+ },
68
96
  ] as const satisfies ReadonlyArray<
69
97
  {
70
98
  table: string;
@@ -73,7 +101,14 @@ export const DB_RETENTION_TABLES = [
73
101
  initialBatchSize?: number;
74
102
  maxBatchSize?: number;
75
103
  } & (
76
- | { policy: "age"; timeColumn: string }
104
+ | {
105
+ policy: "age";
106
+ timeColumn: string;
107
+ cutoffFormat?: "sqliteDatetime";
108
+ statusColumn?: string;
109
+ terminalStatuses?: readonly string[];
110
+ keepWhenSet?: string;
111
+ }
77
112
  | { policy: "keepLatest"; partitionBy: string; rankBy: string }
78
113
  )
79
114
  >;
@@ -326,13 +361,33 @@ export function sweepStatements(table: RetentionTable, value: number, now: Date)
326
361
  params: [value],
327
362
  };
328
363
  }
329
- const cutoff = new Date(now.getTime() - value * DAY_MS).toISOString();
364
+ const iso = new Date(now.getTime() - value * DAY_MS).toISOString();
365
+ // Text comparison of the two formats: ' ' sorts before 'T', so against a
366
+ // space-separated cutoff an ISO row on the cutoff's own date is kept, never
367
+ // deleted early. It is swept on the next day's ticks instead.
368
+ const cutoff = "cutoffFormat" in table ? `${iso.slice(0, 10)} ${iso.slice(11, 19)}` : iso;
369
+ const conditions: string[] = [];
370
+ const params: Array<string | number> = [];
371
+ if ("terminalStatuses" in table) {
372
+ conditions.push(
373
+ `${table.statusColumn} IN (${table.terminalStatuses.map(() => "?").join(", ")})`,
374
+ );
375
+ params.push(...table.terminalStatuses);
376
+ }
377
+ conditions.push(`${table.timeColumn} < ?`);
378
+ params.push(cutoff);
379
+ if ("keepWhenSet" in table) {
380
+ conditions.push(
381
+ `rowid NOT IN (SELECT rowid FROM ${table.table} WHERE ${table.keepWhenSet} IS NOT NULL)`,
382
+ );
383
+ }
384
+ const where = conditions.join(" AND ");
330
385
  return {
331
- backlogSql: `SELECT COUNT(*) AS n FROM ${table.table} WHERE ${table.timeColumn} < ?`,
386
+ backlogSql: `SELECT COUNT(*) AS n FROM ${table.table} WHERE ${where}`,
332
387
  deleteSql: `DELETE FROM ${table.table} WHERE rowid IN (
333
- SELECT rowid FROM ${table.table} WHERE ${table.timeColumn} < ? ORDER BY ${table.timeColumn} LIMIT ?
388
+ SELECT rowid FROM ${table.table} WHERE ${where} ORDER BY ${table.timeColumn} LIMIT ?
334
389
  )`,
335
- params: [cutoff],
390
+ params,
336
391
  };
337
392
  }
338
393