@desplega.ai/agent-swarm 2.5.4 → 2.7.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 (354) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
  7. package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
  18. package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
  19. package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
  20. package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
  21. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  22. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  23. package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
  24. package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
  25. package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
  26. package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
  27. package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
  28. package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
  29. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  30. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  31. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  32. package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
  33. package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
  34. package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
  35. package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
  36. package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
  37. package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
  38. package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
  39. package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
  40. package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
  41. package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
  42. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  43. package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
  44. package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
  45. package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
  46. package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
  47. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  48. package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
  49. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  50. package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
  51. package/dist/cli-gt4h8cxw.js +895 -0
  52. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  53. package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
  54. package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
  55. package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
  56. package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
  57. package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
  58. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  59. package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
  60. package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
  61. package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
  62. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  63. package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
  64. package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
  65. package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
  66. package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
  67. package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
  68. package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
  69. package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
  70. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  71. package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
  72. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  73. package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
  74. package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
  75. package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
  76. package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
  77. package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
  78. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  79. package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
  80. package/dist/cli.js +22 -20
  81. package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
  82. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  83. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  84. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
  85. package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
  86. package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
  87. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
  88. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
  89. package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
  90. package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
  91. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  92. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
  93. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  94. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
  95. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  96. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  97. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  98. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  99. package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
  100. package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
  101. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
  102. package/dist/handlers-tzmevqb5.js +41 -0
  103. package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
  104. package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
  105. package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
  106. package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
  107. package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
  108. package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
  109. package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
  110. package/dist/index-y1ajx04a.js +43 -0
  111. package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
  112. package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
  113. package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
  114. package/dist/lead-hqg57kdp.js +79 -0
  115. package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
  117. package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
  118. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
  119. package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
  120. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
  121. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
  122. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
  123. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
  124. package/dist/render-v2-yfvdfr0x.js +16 -0
  125. package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
  126. package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
  127. package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
  128. package/dist/session-source-backfill-424wzv0r.js +110 -0
  129. package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
  130. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
  131. package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
  132. package/dist/worker-sqjd1nqf.js +79 -0
  133. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  134. package/openapi.json +1324 -14
  135. package/package.json +5 -3
  136. package/src/assets/source-key.ts +95 -0
  137. package/src/be/automation-preflight.ts +2 -0
  138. package/src/be/db/api-keys.ts +89 -0
  139. package/src/be/db/session-labels.ts +28 -0
  140. package/src/be/db/session-moves.ts +5 -6
  141. package/src/be/db/session-tree.ts +153 -14
  142. package/src/be/db/tasks/read.ts +11 -0
  143. package/src/be/db/tasks/write.ts +6 -1
  144. package/src/be/db/workflow-runs.ts +219 -6
  145. package/src/be/db-retention.ts +61 -6
  146. package/src/be/db.ts +135 -132
  147. package/src/be/memory/providers/openai-embedding.ts +5 -1
  148. package/src/be/memory/task-learning.ts +155 -0
  149. package/src/be/migrations/244_api_key_validation.sql +4 -0
  150. package/src/be/migrations/245_script_executor.sql +3 -0
  151. package/src/be/oauth-refresh-sweep.ts +2 -1
  152. package/src/be/onboarding.ts +4 -0
  153. package/src/be/scripts/boot-reembed.ts +2 -0
  154. package/src/be/scripts/db.ts +14 -4
  155. package/src/be/scripts/embeddings.ts +2 -0
  156. package/src/be/scripts/run-saved.ts +1 -0
  157. package/src/be/scripts/typecheck.ts +10 -4
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/session-source-backfill.ts +163 -0
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/comb/review-batch.ts +28 -1
  162. package/src/comb/templates.ts +7 -2
  163. package/src/commands/claude-managed-setup.ts +7 -1
  164. package/src/commands/credential-refresh.ts +76 -1
  165. package/src/commands/credential-wait.ts +2 -2
  166. package/src/commands/onboard/compose-generator.ts +5 -3
  167. package/src/commands/provider-credentials.ts +212 -171
  168. package/src/commands/runner.ts +25 -4
  169. package/src/extensions/apply-task-create.ts +8 -1
  170. package/src/extensions/contract-types.generated.ts +1 -1
  171. package/src/extensions/contract.ts +5 -0
  172. package/src/heartbeat/heartbeat.ts +14 -1
  173. package/src/heartbeat/script-runner-probe.ts +47 -0
  174. package/src/http/all-routes.ts +2 -0
  175. package/src/http/api-keys.ts +77 -9
  176. package/src/http/assets.ts +15 -0
  177. package/src/http/codex-oauth-accounts.ts +191 -0
  178. package/src/http/comb.ts +15 -2
  179. package/src/http/config.ts +1 -1
  180. package/src/http/core.ts +6 -0
  181. package/src/http/fleet-benchmark.ts +5 -2
  182. package/src/http/idempotency-key.ts +1 -1
  183. package/src/http/index.ts +14 -3
  184. package/src/http/integrations.ts +5 -1
  185. package/src/http/kv.ts +8 -3
  186. package/src/http/onboarding.ts +6 -1
  187. package/src/http/pages-public.ts +38 -5
  188. package/src/http/pool-keys.ts +354 -0
  189. package/src/http/scripts.ts +27 -0
  190. package/src/http/sessions.ts +37 -12
  191. package/src/http/stats.ts +1 -0
  192. package/src/http/status.ts +26 -12
  193. package/src/http/tasks.ts +3 -0
  194. package/src/http/used-by.ts +7 -2
  195. package/src/http/workflows.ts +88 -9
  196. package/src/http/x.ts +5 -0
  197. package/src/integrations/chat-sdk/inbound.ts +27 -0
  198. package/src/kv-reserved-namespaces.ts +19 -0
  199. package/src/prompts/base-prompt.ts +21 -1
  200. package/src/prompts/session-templates.ts +17 -3
  201. package/src/providers/acp-targets.ts +1 -1
  202. package/src/providers/amp-adapter.ts +1 -1
  203. package/src/providers/claude-adapter.ts +5 -1
  204. package/src/providers/claude-managed-adapter.ts +16 -9
  205. package/src/providers/codex-adapter.ts +71 -11
  206. package/src/providers/dsh-adapter.ts +1 -1
  207. package/src/providers/pi-mono-adapter.ts +41 -11
  208. package/src/scheduler/scheduler.ts +5 -3
  209. package/src/scripts-runtime/eval-harness.ts +8 -1
  210. package/src/scripts-runtime/executors/native.ts +22 -16
  211. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  212. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  213. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  214. package/src/scripts-runtime/executors/registry.ts +32 -0
  215. package/src/scripts-runtime/executors/types.ts +7 -0
  216. package/src/scripts-runtime/host-validated-args.ts +133 -0
  217. package/src/scripts-runtime/loader.ts +30 -3
  218. package/src/scripts-runtime/swarm-sdk.ts +16 -3
  219. package/src/scripts-runtime/types/stdlib.d.ts +10 -2
  220. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  221. package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
  222. package/src/slack/assistant.ts +7 -0
  223. package/src/slack/channel-labels.ts +74 -0
  224. package/src/slack/handlers.ts +8 -0
  225. package/src/slack/inbound-files.ts +5 -2
  226. package/src/slack/thread-buffer.ts +4 -0
  227. package/src/tasks/sibling-awareness.ts +12 -3
  228. package/src/telemetry-context.ts +5 -6
  229. package/src/telemetry-identity.ts +3 -3
  230. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  231. package/src/tests/api-keys-validation.test.ts +170 -0
  232. package/src/tests/base-prompt.test.ts +70 -2
  233. package/src/tests/claude-managed-adapter.test.ts +46 -0
  234. package/src/tests/claude-routes-e2e.test.ts +7 -2
  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/community-linear-delivery-loop.test.ts +2099 -0
  240. package/src/tests/config-pool-keys.test.ts +533 -0
  241. package/src/tests/credential-check.test.ts +294 -4
  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/docs-index.test.ts +135 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/extensions-pre-task.test.ts +53 -1
  250. package/src/tests/fetch-resolved-env.test.ts +16 -2
  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/fleet-benchmark.test.ts +8 -0
  254. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  255. package/src/tests/heartbeat-checklist.test.ts +89 -0
  256. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  257. package/src/tests/integrations-http.test.ts +32 -2
  258. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  259. package/src/tests/internal-ai/credentials.test.ts +23 -0
  260. package/src/tests/kv-http.test.ts +32 -0
  261. package/src/tests/kv-tool.test.ts +25 -0
  262. package/src/tests/multi-runtime-registration.test.ts +12 -12
  263. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  264. package/src/tests/onboard-compose.test.ts +20 -0
  265. package/src/tests/onboarding.test.ts +108 -1
  266. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  267. package/src/tests/opencode-adapter.test.ts +2 -1
  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/pages-frame-ancestors.test.ts +162 -0
  271. package/src/tests/pi-codemode.test.ts +2 -1
  272. package/src/tests/pi-mono-adapter.test.ts +54 -0
  273. package/src/tests/pi-native-mcp.test.ts +13 -1
  274. package/src/tests/pi-tool-exposure.test.ts +8 -8
  275. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  276. package/src/tests/prompt-template-session.test.ts +4 -3
  277. package/src/tests/provider-key-probes.test.ts +181 -0
  278. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  279. package/src/tests/rbac-e2e-helpers.ts +36 -11
  280. package/src/tests/request-composition-derive.test.ts +329 -0
  281. package/src/tests/script-connections.test.ts +4 -0
  282. package/src/tests/script-executor-conformance.test.ts +36 -1
  283. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  284. package/src/tests/script-executor-quickjs.test.ts +140 -0
  285. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  286. package/src/tests/script-per-script-executor.test.ts +415 -0
  287. package/src/tests/script-runs-retention.test.ts +212 -0
  288. package/src/tests/scripts-external-api.test.ts +45 -0
  289. package/src/tests/sdk-allowlist.test.ts +42 -2
  290. package/src/tests/self-improvement.test.ts +4 -4
  291. package/src/tests/session-source-backfill.test.ts +429 -0
  292. package/src/tests/session-source-keys.test.ts +236 -0
  293. package/src/tests/session-source-labels.test.ts +419 -0
  294. package/src/tests/sessions-detail-batch.test.ts +2 -0
  295. package/src/tests/sessions-move.test.ts +131 -0
  296. package/src/tests/sessions-tree.test.ts +299 -2
  297. package/src/tests/slack-inbound-files.test.ts +103 -23
  298. package/src/tests/status.test.ts +101 -18
  299. package/src/tests/store-progress-learning.test.ts +288 -0
  300. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  301. package/src/tests/test-net.ts +31 -0
  302. package/src/tests/transcription.test.ts +8 -0
  303. package/src/tests/ui-logs-parser.test.ts +180 -0
  304. package/src/tests/used-by-route.test.ts +11 -0
  305. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  306. package/src/tests/workflow-engine-v2.test.ts +116 -1
  307. package/src/tests/workflow-http-v2.test.ts +131 -0
  308. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  309. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  310. package/src/tests/workflow-step-order.test.ts +92 -0
  311. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  312. package/src/tests/workflow-swarm-script.test.ts +34 -0
  313. package/src/tools/get-tasks.ts +8 -0
  314. package/src/tools/kv/kv-read-auth.ts +4 -2
  315. package/src/tools/script-common.ts +1 -0
  316. package/src/tools/script-run.ts +7 -0
  317. package/src/tools/script-upsert.ts +7 -0
  318. package/src/tools/store-progress.ts +67 -1
  319. package/src/tools/task-action.ts +1 -1
  320. package/src/tools/workflows/get-workflow-run.ts +14 -4
  321. package/src/tools/workflows/trigger-workflow.ts +104 -10
  322. package/src/types.ts +36 -1
  323. package/src/utils/company-profile.ts +51 -1
  324. package/src/utils/credentials.ts +10 -4
  325. package/src/utils/internal-ai/complete-structured.ts +5 -0
  326. package/src/utils/internal-ai/credentials.ts +4 -3
  327. package/src/utils/provider-key-probes.ts +328 -0
  328. package/src/utils/transcription.ts +4 -1
  329. package/src/workflows/engine.ts +18 -16
  330. package/src/workflows/executors/sub-workflow.ts +8 -1
  331. package/src/workflows/executors/swarm-script.ts +5 -0
  332. package/src/workflows/foreach-driver.ts +5 -11
  333. package/src/workflows/node-timeout.ts +30 -0
  334. package/templates/ai-toolbox.manifest.json +2 -2
  335. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  336. package/templates/community/linear-delivery-loop/README.md +38 -0
  337. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  338. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  339. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  340. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  341. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  342. package/templates/skills/researching/content.md +2 -0
  343. package/templates/skills/swarm-docs/config.json +14 -0
  344. package/templates/skills/swarm-docs/content.md +144 -0
  345. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  346. package/templates/skills/swarm-scripts/content.md +1 -0
  347. package/dist/cli-7421jzsw.js +0 -84
  348. package/dist/cli-e45e4m6b.js +0 -413
  349. package/dist/cli-zw88zjba.js +0 -307
  350. package/dist/handlers-wy1dr6ja.js +0 -41
  351. package/dist/index-nmm79qgr.js +0 -43
  352. package/dist/lead-sawnyqxk.js +0 -79
  353. package/dist/render-v2-pjwxvnkw.js +0 -12
  354. package/dist/worker-v5e5rxj3.js +0 -79
@@ -1,10 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { normalizeSessionLogs as normalizeEvalLogs } from "../../apps/evals/ui/src/logs-parser";
3
3
  import {
4
+ cacheHitRatio,
4
5
  extractSubagentRuns,
5
6
  normalizeSessionLogs,
6
7
  parseSessionLogs,
7
8
  type SessionLogRecord,
9
+ sumItemCallUsage,
8
10
  unwrapResult,
9
11
  } from "../../apps/ui/src/logs-parser";
10
12
 
@@ -1696,3 +1698,181 @@ describe.each([
1696
1698
  expect(item?.meta).toMatchObject({ type: "codex_error", isError: true });
1697
1699
  });
1698
1700
  });
1701
+
1702
+ describe("per-call usage", () => {
1703
+ const claudeUsage = {
1704
+ input_tokens: 6,
1705
+ cache_read_input_tokens: 90_000,
1706
+ cache_creation_input_tokens: 684,
1707
+ output_tokens: 120,
1708
+ };
1709
+ function claudeLine(
1710
+ id: string,
1711
+ line: number,
1712
+ messageId: string,
1713
+ block: unknown,
1714
+ extra: Record<string, unknown> = {},
1715
+ ) {
1716
+ return log(id, "claude", line, {
1717
+ type: "assistant",
1718
+ message: { id: messageId, role: "assistant", content: [block], usage: claudeUsage },
1719
+ ...extra,
1720
+ });
1721
+ }
1722
+ const usages = (logs: SessionLogRecord[]) =>
1723
+ normalizeSessionLogs(logs)
1724
+ .items.filter((item) => item.kind === "call_usage")
1725
+ .map((item) => item.usage);
1726
+
1727
+ test("claude: one call_usage per message.id, not per content-block line", () => {
1728
+ const result = usages([
1729
+ claudeLine("a1", 1, "msg_1", { type: "thinking", thinking: "" }),
1730
+ claudeLine("a2", 2, "msg_1", { type: "text", text: "Looking." }),
1731
+ claudeLine("a3", 3, "msg_1", { type: "tool_use", id: "t1", name: "Read", input: {} }),
1732
+ claudeLine(
1733
+ "s1",
1734
+ 4,
1735
+ "msg_2",
1736
+ { type: "text", text: "sub" },
1737
+ {
1738
+ parent_tool_use_id: "toolu_agent",
1739
+ },
1740
+ ),
1741
+ ]);
1742
+ expect(result).toHaveLength(2);
1743
+ expect(result[0]).toMatchObject({
1744
+ callId: "msg_1",
1745
+ promptTokens: 90_690,
1746
+ cacheReadTokens: 90_000,
1747
+ cacheWriteTokens: 684,
1748
+ uncachedInputTokens: 6,
1749
+ outputTokens: 120,
1750
+ sidechain: false,
1751
+ harness: "claude",
1752
+ reportedAt: "start",
1753
+ });
1754
+ expect(result[1]).toMatchObject({ callId: "msg_2", sidechain: true });
1755
+ expect(cacheHitRatio(result[0]!)).toBeCloseTo(90_000 / 90_690);
1756
+ });
1757
+
1758
+ test("claude: an assistant line without usage emits no call_usage", () => {
1759
+ const result = usages([
1760
+ log("a1", "claude", 1, {
1761
+ type: "assistant",
1762
+ message: { id: "msg_1", role: "assistant", content: [{ type: "text", text: "hi" }] },
1763
+ }),
1764
+ ]);
1765
+ expect(result).toEqual([]);
1766
+ });
1767
+
1768
+ test("the call_usage block shares the call's message, ahead of its content", () => {
1769
+ const messages = parseSessionLogs([
1770
+ claudeLine("a1", 1, "msg_1", { type: "text", text: "Looking." }),
1771
+ ]);
1772
+ expect(messages).toHaveLength(1);
1773
+ expect(messages[0]?.role).toBe("assistant");
1774
+ expect(messages[0]?.content.map((b) => b.type)).toEqual(["provider_meta", "text"]);
1775
+ expect(messages[0]?.content[0]).toMatchObject({
1776
+ kind: "helper",
1777
+ data: { helperType: "call_usage" },
1778
+ });
1779
+ });
1780
+
1781
+ test("codex: context.updated input_tokens already includes cached tokens", () => {
1782
+ const result = usages([
1783
+ log("c1", "codex", 1, {
1784
+ type: "context.updated",
1785
+ usage: {
1786
+ input_tokens: 49_300,
1787
+ cached_input_tokens: 12_000,
1788
+ cache_write_input_tokens: 0,
1789
+ output_tokens: 300,
1790
+ reasoning_output_tokens: 40,
1791
+ },
1792
+ contextWindow: 272_000,
1793
+ }),
1794
+ ]);
1795
+ expect(result).toEqual([
1796
+ {
1797
+ callId: "c1",
1798
+ promptTokens: 49_300,
1799
+ cacheReadTokens: 12_000,
1800
+ cacheWriteTokens: 0,
1801
+ uncachedInputTokens: 37_300,
1802
+ outputTokens: 300,
1803
+ reasoningTokens: 40,
1804
+ sidechain: false,
1805
+ harness: "codex",
1806
+ reportedAt: "end",
1807
+ },
1808
+ ]);
1809
+ expect(cacheHitRatio(result[0]!)).toBeCloseTo(12_000 / 49_300);
1810
+ });
1811
+
1812
+ test("opencode: session.step.ended input excludes cache", () => {
1813
+ const result = usages([
1814
+ log("o1", "opencode", 1, {
1815
+ type: "session.step.ended",
1816
+ data: {
1817
+ sessionID: "ses_1",
1818
+ tokens: { input: 200, output: 50, reasoning: 5, cache: { read: 9_000, write: 800 } },
1819
+ },
1820
+ }),
1821
+ ]);
1822
+ expect(result).toHaveLength(1);
1823
+ expect(result[0]).toMatchObject({
1824
+ promptTokens: 10_000,
1825
+ cacheReadTokens: 9_000,
1826
+ cacheWriteTokens: 800,
1827
+ uncachedInputTokens: 200,
1828
+ harness: "opencode",
1829
+ reportedAt: "end",
1830
+ });
1831
+ expect(cacheHitRatio(result[0]!)).toBeCloseTo(0.9);
1832
+ });
1833
+
1834
+ test("pi: the adapter's call_usage line", () => {
1835
+ const result = usages([
1836
+ log("p1", "pi", 1, {
1837
+ type: "call_usage",
1838
+ harness: "pi",
1839
+ usage: { input: 100, cacheRead: 300, cacheWrite: 100, output: 20, reasoning: 0 },
1840
+ }),
1841
+ ]);
1842
+ expect(result).toHaveLength(1);
1843
+ expect(result[0]).toMatchObject({
1844
+ promptTokens: 500,
1845
+ cacheReadTokens: 300,
1846
+ harness: "pi",
1847
+ reportedAt: "start",
1848
+ });
1849
+ expect(cacheHitRatio(result[0]!)).toBeCloseTo(0.6);
1850
+ });
1851
+
1852
+ test("session totals: Σ read / Σ prompt, and no-cache sessions say so", () => {
1853
+ const totals = sumItemCallUsage(
1854
+ normalizeSessionLogs([
1855
+ claudeLine("a1", 1, "msg_1", { type: "text", text: "one" }),
1856
+ claudeLine("a2", 2, "msg_2", { type: "text", text: "two" }),
1857
+ ]).items,
1858
+ );
1859
+ expect(totals).toMatchObject({
1860
+ calls: 2,
1861
+ promptTokens: 181_380,
1862
+ cacheReadTokens: 180_000,
1863
+ cacheReported: true,
1864
+ });
1865
+ expect(totals?.ratio).toBeCloseTo(180_000 / 181_380);
1866
+
1867
+ const uncached = sumItemCallUsage(
1868
+ normalizeSessionLogs([
1869
+ log("o1", "opencode", 1, {
1870
+ type: "session.step.ended",
1871
+ data: { tokens: { input: 500, output: 5, reasoning: 0, cache: { read: 0, write: 0 } } },
1872
+ }),
1873
+ ]).items,
1874
+ );
1875
+ expect(uncached).toMatchObject({ calls: 1, cacheReported: false, ratio: 0 });
1876
+ expect(sumItemCallUsage([])).toBeNull();
1877
+ });
1878
+ });
@@ -183,6 +183,17 @@ describe("POST /api/used-by", () => {
183
183
  expect(proxied[1]?.body).not.toHaveProperty("quote");
184
184
  });
185
185
 
186
+ test("a job function the catalog does not list yet is stored but not sent", async () => {
187
+ const grant = await post(
188
+ { granted: true, contactEmail: "admin@acme.com", contactFunction: "marketing" },
189
+ OPERATOR,
190
+ );
191
+ expect(grant.statusCode).toBe(200);
192
+ expect(proxied[0]?.body).toMatchObject({ granted: true, contact_email: "admin@acme.com" });
193
+ expect(proxied[0]?.body).not.toHaveProperty("contact_function");
194
+ expect(await configValue("SWARM_USED_BY_CONTACT_FUNCTION")).toBe("marketing");
195
+ });
196
+
186
197
  test("a grant requires valid contact details and a quote of at most 280 characters", async () => {
187
198
  for (const details of [
188
199
  {},
@@ -26,6 +26,7 @@ import {
26
26
  WorkflowStepNotExecutableError,
27
27
  } from "../be/db/workflow-runs";
28
28
  import { handleApprovalRequests } from "../http/approval-requests";
29
+ import { handleWorkflows } from "../http/workflows";
29
30
  import * as slackApp from "../slack/app";
30
31
  import type { AgentTask, Workflow, WorkflowDefinition, WorkflowRun } from "../types";
31
32
  import * as approvalNotifications from "../workflows/approval-notifications";
@@ -1167,3 +1168,174 @@ describe("run lineage guard", () => {
1167
1168
  ).toBe("failed");
1168
1169
  });
1169
1170
  });
1171
+
1172
+ describe("run tree read (GET /api/workflow-runs/{id}/tree)", () => {
1173
+ let server: ReturnType<typeof createServer>;
1174
+ let baseUrl = "";
1175
+
1176
+ beforeAll(async () => {
1177
+ server = createServer(async (req, res) => {
1178
+ const url = new URL(req.url ?? "/", "http://localhost");
1179
+ const handled = await handleWorkflows(
1180
+ req,
1181
+ res,
1182
+ url.pathname.split("/").filter(Boolean),
1183
+ url.searchParams,
1184
+ undefined,
1185
+ );
1186
+ if (!handled) {
1187
+ res.writeHead(404, { "Content-Type": "application/json" });
1188
+ res.end(JSON.stringify({ error: "Not found" }));
1189
+ }
1190
+ });
1191
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
1192
+ });
1193
+
1194
+ afterAll(async () => {
1195
+ await new Promise<void>((resolve) => server.close(() => resolve()));
1196
+ });
1197
+
1198
+ type TreeBody = {
1199
+ rootRunId: string;
1200
+ runs: Array<WorkflowRun & Record<string, unknown>>;
1201
+ hasOpenWork: boolean;
1202
+ error?: string;
1203
+ };
1204
+
1205
+ async function getTree(runId: string): Promise<{ status: number; body: TreeBody | null }> {
1206
+ const response = await fetch(`${baseUrl}/api/workflow-runs/${runId}/tree`);
1207
+ const text = await response.text();
1208
+ return { status: response.status, body: text ? (JSON.parse(text) as TreeBody) : null };
1209
+ }
1210
+
1211
+ test("lists every run of every attempt, root first, and settles after a cascade cancel", async () => {
1212
+ const grandchild = await makeWorkflow(taskDef);
1213
+ const child = await makeWorkflow(parentOf(grandchild.id));
1214
+ const root = await makeWorkflow(parentOf(child.id));
1215
+ const rootId = await startWorkflowExecution(root, {}, registry);
1216
+ const childRun = await runOf(child.id, "waiting");
1217
+ const grandchildRun = await runOf(grandchild.id, "waiting");
1218
+ const [task] = await waitFor(
1219
+ () => tasksOfRun(grandchildRun.id),
1220
+ (tasks) => tasks.length === 1,
1221
+ );
1222
+
1223
+ // A retry of the root step: the live child is attempt 1, and attempt 0's
1224
+ // child failed after starting a grandchild of its own.
1225
+ const [rootStep] = await getWorkflowRunStepsByRunId(rootId);
1226
+ await getDbClient().run("UPDATE workflow_run_steps SET retryCount = 1 WHERE id = ?", [
1227
+ rootStep!.id,
1228
+ ]);
1229
+ await getDbClient().run("UPDATE workflow_runs SET parentStepAttempt = 1 WHERE id = ?", [
1230
+ childRun.id,
1231
+ ]);
1232
+ const oldChild = await linkedRun({
1233
+ workflowId: child.id,
1234
+ status: "failed",
1235
+ parentStepId: rootStep!.id,
1236
+ parentStepAttempt: 0,
1237
+ });
1238
+ const oldChildStep = await linkedStep(oldChild, {
1239
+ nodeId: "child0",
1240
+ nodeType: "sub-workflow",
1241
+ status: "failed",
1242
+ });
1243
+ const oldGrandchild = await linkedRun({
1244
+ workflowId: grandchild.id,
1245
+ status: "failed",
1246
+ parentStepId: oldChildStep,
1247
+ });
1248
+
1249
+ const before = await getTree(rootId);
1250
+ expect(before.status).toBe(200);
1251
+ expect(before.body!.rootRunId).toBe(rootId);
1252
+ expect(before.body!.hasOpenWork).toBe(true);
1253
+ const ids = before.body!.runs.map((run) => run.id);
1254
+ expect(ids[0]).toBe(rootId);
1255
+ expect(new Set(ids.slice(1, 3))).toEqual(new Set([childRun.id, oldChild]));
1256
+ expect(new Set(ids.slice(3))).toEqual(new Set([grandchildRun.id, oldGrandchild]));
1257
+ const byId = new Map(before.body!.runs.map((run) => [run.id, run]));
1258
+ expect(byId.get(childRun.id)).toMatchObject({
1259
+ workflowId: child.id,
1260
+ status: "waiting",
1261
+ parentStepId: rootStep!.id,
1262
+ parentStepAttempt: 1,
1263
+ });
1264
+ expect(byId.get(oldChild)).toMatchObject({ status: "failed", parentStepAttempt: 0 });
1265
+ expect(byId.get(oldGrandchild)?.parentStepId).toBe(oldChildStep);
1266
+ for (const run of before.body!.runs) expect("context" in run).toBe(false);
1267
+
1268
+ await cancelWorkflowRun(rootId, "stop the tree");
1269
+ expect((await getTaskById(task!.id))?.status).toBe("cancelled");
1270
+
1271
+ const after = await getTree(rootId);
1272
+ expect(after.status).toBe(200);
1273
+ expect(after.body!.hasOpenWork).toBe(false);
1274
+ expect(after.body!.runs.map((run) => run.id)).toEqual(ids);
1275
+ const statuses = new Map(after.body!.runs.map((run) => [run.id, run.status]));
1276
+ expect(statuses.get(rootId)).toBe("cancelled");
1277
+ expect(statuses.get(childRun.id)).toBe("cancelled");
1278
+ expect(statuses.get(grandchildRun.id)).toBe("cancelled");
1279
+ // Terminal history keeps its status.
1280
+ expect(statuses.get(oldChild)).toBe("failed");
1281
+ expect(statuses.get(oldGrandchild)).toBe("failed");
1282
+
1283
+ // A subtree reads on its own, without its ancestors.
1284
+ const subtree = await getTree(childRun.id);
1285
+ expect(subtree.body!.runs.map((run) => run.id)).toEqual([childRun.id, grandchildRun.id]);
1286
+ });
1287
+
1288
+ test("a tree wider than one summary query returns every run once, in walk order", async () => {
1289
+ const wf = await makeWorkflow({ nodes: [] });
1290
+ const rootId = await linkedRun({ workflowId: wf.id, status: "completed" });
1291
+ const step = await linkedStep(rootId, {
1292
+ nodeId: "fan",
1293
+ nodeType: "sub-workflow",
1294
+ status: "completed",
1295
+ });
1296
+ // One child per attempt, over two 500-id summary chunks, so a dropped or
1297
+ // repeated chunk shows.
1298
+ const childCount = 1_100;
1299
+ await getDbClient().transaction(async () => {
1300
+ for (let i = 0; i < childCount; i++) {
1301
+ await createWorkflowRun({
1302
+ id: crypto.randomUUID(),
1303
+ workflowId: wf.id,
1304
+ parentStepId: step,
1305
+ parentStepAttempt: i,
1306
+ });
1307
+ }
1308
+ });
1309
+
1310
+ const walk = await getWorkflowRunTreeIds(rootId);
1311
+ expect(walk).toHaveLength(childCount + 1);
1312
+ const tree = await getTree(rootId);
1313
+ expect(tree.status).toBe(200);
1314
+ expect(tree.body!.runs.map((run) => run.id)).toEqual(walk);
1315
+ expect(tree.body!.hasOpenWork).toBe(true);
1316
+ });
1317
+
1318
+ test("a run without children is its own tree", async () => {
1319
+ const wf = await makeWorkflow({ nodes: [] });
1320
+ const runId = await linkedRun({ workflowId: wf.id, status: "completed" });
1321
+ const tree = await getTree(runId);
1322
+ expect(tree.status).toBe(200);
1323
+ expect(tree.body!.runs.map((run) => run.id)).toEqual([runId]);
1324
+ expect(tree.body!.hasOpenWork).toBe(false);
1325
+ });
1326
+
1327
+ test("an unknown run is 404 and a cycle in the stored links is 409", async () => {
1328
+ expect((await getTree(crypto.randomUUID())).status).toBe(404);
1329
+
1330
+ const wf = await makeWorkflow({ nodes: [] });
1331
+ const a = await linkedRun({ workflowId: wf.id, status: "waiting" });
1332
+ const aStep = await linkedStep(a, { nodeId: "s", nodeType: "sub-workflow", status: "waiting" });
1333
+ const b = await linkedRun({ workflowId: wf.id, status: "waiting", parentStepId: aStep });
1334
+ const bStep = await linkedStep(b, { nodeId: "s", nodeType: "sub-workflow", status: "waiting" });
1335
+ await getDbClient().run("UPDATE workflow_runs SET parentStepId = ? WHERE id = ?", [bStep, a]);
1336
+
1337
+ const cycle = await getTree(a);
1338
+ expect(cycle.status).toBe(409);
1339
+ expect(cycle.body!.error).toContain("cycle");
1340
+ });
1341
+ });
@@ -21,7 +21,7 @@ import {
21
21
  getWorkflowRunStepsByRunId,
22
22
  initDb,
23
23
  } from "../be/db";
24
- import type { Workflow, WorkflowDefinition } from "../types";
24
+ import type { ExecutorMeta, Workflow, WorkflowDefinition } from "../types";
25
25
  import { shouldSkipCooldown } from "../workflows/cooldown";
26
26
  import {
27
27
  findReadyNodes,
@@ -143,6 +143,45 @@ class SlowExecutor extends BaseExecutor<typeof SlowExecutor.schema, typeof SlowE
143
143
  }
144
144
  }
145
145
 
146
+ /** What the last `signal-probe` step saw: its abort signal and whether it got to its side effect. */
147
+ const signalProbe: { signal?: AbortSignal; sideEffect: boolean } = { sideEffect: false };
148
+
149
+ class SignalProbeExecutor extends BaseExecutor<
150
+ typeof SignalProbeExecutor.schema,
151
+ typeof SignalProbeExecutor.outSchema
152
+ > {
153
+ static readonly schema = z.object({ delayMs: z.number() });
154
+ static readonly outSchema = z.object({ finished: z.boolean() });
155
+
156
+ readonly type = "signal-probe";
157
+ readonly mode = "instant" as const;
158
+ readonly configSchema = SignalProbeExecutor.schema;
159
+ readonly outputSchema = SignalProbeExecutor.outSchema;
160
+
161
+ protected async execute(
162
+ config: z.infer<typeof SignalProbeExecutor.schema>,
163
+ _context: Readonly<Record<string, unknown>>,
164
+ meta: ExecutorMeta,
165
+ ): Promise<ExecutorResult<z.infer<typeof SignalProbeExecutor.outSchema>>> {
166
+ const signal = meta.signal;
167
+ signalProbe.signal = signal;
168
+ await new Promise<void>((resolve) => {
169
+ const timer = setTimeout(resolve, config.delayMs);
170
+ signal?.addEventListener(
171
+ "abort",
172
+ () => {
173
+ clearTimeout(timer);
174
+ resolve();
175
+ },
176
+ { once: true },
177
+ );
178
+ });
179
+ // A one-shot action, like the review trigger a claim script fires.
180
+ if (!signal?.aborted) signalProbe.sideEffect = true;
181
+ return { status: "success", output: { finished: true } };
182
+ }
183
+ }
184
+
146
185
  class FailingExecutor extends BaseExecutor<
147
186
  typeof FailingExecutor.schema,
148
187
  typeof FailingExecutor.outSchema
@@ -216,6 +255,7 @@ function createTestRegistry(): ExecutorRegistry {
216
255
  registry.register(new SlowExecutor(mockDeps));
217
256
  registry.register(new FailingExecutor(mockDeps));
218
257
  registry.register(new ValidatePassExecutor(mockDeps));
258
+ registry.register(new SignalProbeExecutor(mockDeps));
219
259
  return registry;
220
260
  }
221
261
 
@@ -561,6 +601,41 @@ describe("Workflow Engine v2 (Phase 3)", () => {
561
601
  expect(run!.error).toContain("timed out");
562
602
  }, 5_000); // Allow test up to 5s
563
603
 
604
+ test("a timed-out step aborts its executor, so the side effect never happens", async () => {
605
+ signalProbe.signal = undefined;
606
+ signalProbe.sideEffect = false;
607
+ const registry = createTestRegistry();
608
+ const workflow = await makeWorkflow({
609
+ nodes: [{ id: "probe", type: "signal-probe", config: { delayMs: 600, timeoutMs: 100 } }],
610
+ });
611
+
612
+ const runId = await startWorkflowExecution(workflow, {}, registry);
613
+
614
+ const run = await getWorkflowRun(runId);
615
+ expect(run!.status).toBe("failed");
616
+ expect(run!.error).toContain("Step timed out after 100ms");
617
+ expect(signalProbe.signal?.aborted).toBe(true);
618
+ // Past the executor's own delay: without the abort it would have fired.
619
+ await Bun.sleep(700);
620
+ expect(signalProbe.sideEffect).toBe(false);
621
+ }, 5_000);
622
+
623
+ test("a step that finishes in budget is never aborted afterwards", async () => {
624
+ signalProbe.signal = undefined;
625
+ signalProbe.sideEffect = false;
626
+ const registry = createTestRegistry();
627
+ const workflow = await makeWorkflow({
628
+ nodes: [{ id: "probe", type: "signal-probe", config: { delayMs: 10, timeoutMs: 150 } }],
629
+ });
630
+
631
+ const runId = await startWorkflowExecution(workflow, {}, registry);
632
+
633
+ expect((await getWorkflowRun(runId))!.status).toBe("completed");
634
+ expect(signalProbe.sideEffect).toBe(true);
635
+ await Bun.sleep(250);
636
+ expect(signalProbe.signal?.aborted).toBe(false);
637
+ }, 5_000);
638
+
564
639
  // The inline runtime's ulimit preamble fails on macOS (no usable
565
640
  // RLIMIT_AS), so the script dies before the watchdog behavior under test.
566
641
  // Linux CI is the enforcing environment; the skip only unblocks local
@@ -598,6 +673,46 @@ describe("Workflow Engine v2 (Phase 3)", () => {
598
673
 
599
674
  // ─── Validation ───────────────────────────────────────────
600
675
 
676
+ describe("Detached start", () => {
677
+ test("detach returns once the run is committed and walks its instant steps in the background", async () => {
678
+ const registry = createTestRegistry();
679
+ const workflow = await makeWorkflow({
680
+ nodes: [
681
+ { id: "slow", type: "slow", config: { delayMs: 400 }, next: "after" },
682
+ { id: "after", type: "echo", config: { message: "done" } },
683
+ ],
684
+ });
685
+
686
+ const started = performance.now();
687
+ const runId = await startWorkflowExecution(workflow, {}, registry, { detach: true });
688
+ const returnedAfterMs = performance.now() - started;
689
+
690
+ expect(returnedAfterMs).toBeLessThan(300);
691
+ expect((await getWorkflowRun(runId))!.status).toBe("running");
692
+
693
+ const deadline = Date.now() + 3_000;
694
+ while ((await getWorkflowRun(runId))!.status === "running" && Date.now() < deadline) {
695
+ await Bun.sleep(25);
696
+ }
697
+ expect((await getWorkflowRun(runId))!.status).toBe("completed");
698
+ const steps = await getWorkflowRunStepsByRunId(runId);
699
+ expect(steps.map((step) => step.nodeId).sort()).toEqual(["after", "slow"]);
700
+ }, 5_000);
701
+
702
+ test("without detach the start resolves only after the instant steps ran", async () => {
703
+ const registry = createTestRegistry();
704
+ const workflow = await makeWorkflow({
705
+ nodes: [{ id: "slow", type: "slow", config: { delayMs: 300 } }],
706
+ });
707
+
708
+ const started = performance.now();
709
+ const runId = await startWorkflowExecution(workflow, {}, registry);
710
+
711
+ expect(performance.now() - started).toBeGreaterThanOrEqual(290);
712
+ expect((await getWorkflowRun(runId))!.status).toBe("completed");
713
+ }, 5_000);
714
+ });
715
+
601
716
  describe("Validation", () => {
602
717
  test("validation pass allows step to complete", async () => {
603
718
  const registry = createTestRegistry();
@@ -20,6 +20,7 @@ import {
20
20
  initDb,
21
21
  listWorkflowRuns,
22
22
  updateWorkflowRun,
23
+ updateWorkflowRunStep,
23
24
  } from "../be/db";
24
25
  import { getPathSegments, parseQueryParams } from "../http/utils";
25
26
  import { handleWorkflows } from "../http/workflows";
@@ -1007,6 +1008,136 @@ describe("Workflow HTTP API v2", () => {
1007
1008
  });
1008
1009
  expect(res.status).toBe(404);
1009
1010
  });
1011
+
1012
+ type RunDetail = {
1013
+ run: WorkflowRun;
1014
+ steps: WorkflowRunStep[];
1015
+ parent?: { runId: string; workflowId: string; stepId: string; nodeId: string };
1016
+ };
1017
+
1018
+ async function getRunDetail(runId: string, fields?: "full" | "slim"): Promise<RunDetail> {
1019
+ const query = fields ? `?fields=${fields}` : "";
1020
+ const res = await fetch(`${baseUrl}/api/workflow-runs/${runId}${query}`, { headers });
1021
+ expect(res.status).toBe(200);
1022
+ return (await res.json()) as RunDetail;
1023
+ }
1024
+
1025
+ /** A finished run whose `poll` node ran `iterations` times, each step snapshotting the context. */
1026
+ async function createLoopRun(iterations: number) {
1027
+ const workflow = await createTestWorkflow();
1028
+ const runId = crypto.randomUUID();
1029
+ await createWorkflowRun({ id: runId, workflowId: workflow.id });
1030
+ const context = {
1031
+ trigger: { repo: "a/b" },
1032
+ poll: { attempts: iterations },
1033
+ blob: "x".repeat(4_000),
1034
+ };
1035
+ for (let i = 0; i < iterations; i++) {
1036
+ const step = await createWorkflowRunStep({
1037
+ id: crypto.randomUUID(),
1038
+ runId,
1039
+ nodeId: "poll",
1040
+ nodeType: "script",
1041
+ input: { ...context, iteration: i },
1042
+ idempotencyKey: `${runId}:poll:${i}`,
1043
+ });
1044
+ await updateWorkflowRunStep(step.id, {
1045
+ status: i === iterations - 1 ? "completed" : "failed",
1046
+ output: { iteration: i, ready: i === iterations - 1 },
1047
+ error: i === iterations - 1 ? null : `not ready (${i})`,
1048
+ diagnostics: `iteration ${i}`,
1049
+ retryCount: i,
1050
+ nextRetryAt: new Date(Date.now() + 60_000).toISOString(),
1051
+ finishedAt: new Date().toISOString(),
1052
+ });
1053
+ }
1054
+ await updateWorkflowRun(runId, {
1055
+ status: "completed",
1056
+ context,
1057
+ finishedAt: new Date().toISOString(),
1058
+ });
1059
+ return { workflow, runId, context };
1060
+ }
1061
+
1062
+ test("fields=slim omits run.context and steps[].input, keeps every other field", async () => {
1063
+ const { runId, context } = await createLoopRun(3);
1064
+
1065
+ const byDefault = await getRunDetail(runId);
1066
+ const full = await getRunDetail(runId, "full");
1067
+ const slim = await getRunDetail(runId, "slim");
1068
+
1069
+ expect(byDefault).toEqual(full);
1070
+ expect(full.run.context).toEqual(context);
1071
+ expect(full.steps).toHaveLength(3);
1072
+ for (const step of full.steps) expect(step.input).toBeDefined();
1073
+
1074
+ expect(slim.run).not.toHaveProperty("context");
1075
+ expect(slim.steps).toHaveLength(3);
1076
+ for (const step of slim.steps) expect(step).not.toHaveProperty("input");
1077
+
1078
+ const { context: _context, ...fullRunRest } = full.run;
1079
+ expect(slim.run).toEqual(fullRunRest);
1080
+ const fullStepsRest = full.steps.map(({ input: _input, ...rest }) => rest);
1081
+ // Same steps in the same order: both reads break `startedAt` ties by rowid.
1082
+ expect(slim.steps).toEqual(fullStepsRest);
1083
+ // The retry and result fields the slim view exists to serve.
1084
+ const last = slim.steps.at(-1)!;
1085
+ expect(last.output).toEqual({ iteration: 2, ready: true });
1086
+ expect(last.retryCount).toBe(2);
1087
+ expect(last.diagnostics).toBe("iteration 2");
1088
+ expect(last.nextRetryAt).toBeDefined();
1089
+ expect(slim.steps[0]!.error).toBe("not ready (0)");
1090
+
1091
+ expect(JSON.stringify(slim).length).toBeLessThan(JSON.stringify(full).length / 4);
1092
+ });
1093
+
1094
+ test("fields=slim returns 404 for missing run", async () => {
1095
+ const res = await fetch(`${baseUrl}/api/workflow-runs/${crypto.randomUUID()}?fields=slim`, {
1096
+ headers,
1097
+ });
1098
+ expect(res.status).toBe(404);
1099
+ });
1100
+
1101
+ test("rejects an unknown fields value", async () => {
1102
+ const { runId } = await createLoopRun(1);
1103
+ const res = await fetch(`${baseUrl}/api/workflow-runs/${runId}?fields=tiny`, { headers });
1104
+ expect(res.status).toBe(400);
1105
+ });
1106
+
1107
+ test("child run carries parent in both views", async () => {
1108
+ const { workflow: parentWorkflow, runId: parentRunId } = await createLoopRun(1);
1109
+ const parentStep = await createWorkflowRunStep({
1110
+ id: crypto.randomUUID(),
1111
+ runId: parentRunId,
1112
+ nodeId: "child",
1113
+ nodeType: "sub-workflow",
1114
+ input: { large: "y".repeat(1_000) },
1115
+ });
1116
+ const childWorkflow = await createTestWorkflow();
1117
+ const childRunId = crypto.randomUUID();
1118
+ await createWorkflowRun({
1119
+ id: childRunId,
1120
+ workflowId: childWorkflow.id,
1121
+ parentStepId: parentStep.id,
1122
+ parentStepAttempt: 0,
1123
+ });
1124
+
1125
+ const expectedParent = {
1126
+ runId: parentRunId,
1127
+ workflowId: parentWorkflow.id,
1128
+ stepId: parentStep.id,
1129
+ nodeId: "child",
1130
+ };
1131
+ const full = await getRunDetail(childRunId, "full");
1132
+ const slim = await getRunDetail(childRunId, "slim");
1133
+ expect(full.parent).toEqual(expectedParent);
1134
+ expect(slim.parent).toEqual(expectedParent);
1135
+ expect(slim.run.parentStepId).toBe(parentStep.id);
1136
+ expect(slim.run.parentStepAttempt).toBe(0);
1137
+
1138
+ // A top-level run has no parent in either view.
1139
+ expect(await getRunDetail(parentRunId, "slim")).not.toHaveProperty("parent");
1140
+ });
1010
1141
  });
1011
1142
 
1012
1143
  // ─── WEBHOOK ──────────────────────────────────────────────