@desplega.ai/agent-swarm 2.6.0 → 2.7.1

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 (324) hide show
  1. package/README.md +2 -0
  2. package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
  3. package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
  4. package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
  5. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  6. package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
  7. package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
  8. package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
  9. package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
  10. package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  11. package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
  12. package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
  13. package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
  14. package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
  15. package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
  16. package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
  17. package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
  18. package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
  19. package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
  20. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  21. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  22. package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
  23. package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
  24. package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
  25. package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
  26. package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
  27. package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
  28. package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
  29. package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
  30. package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
  31. package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
  32. package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
  33. package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
  34. package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
  35. package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
  36. package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
  37. package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
  38. package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
  39. package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
  40. package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
  41. package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
  42. package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
  43. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  44. package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
  45. package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
  46. package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
  47. package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
  48. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  49. package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
  50. package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
  51. package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
  52. package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
  53. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  54. package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
  55. package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
  56. package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
  57. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  58. package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
  59. package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
  60. package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
  61. package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
  62. package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
  63. package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
  64. package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
  65. package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
  66. package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
  67. package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
  68. package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
  69. package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
  70. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  71. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  72. package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
  73. package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
  74. package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
  75. package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
  76. package/dist/cli.js +31 -21
  77. package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
  78. package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
  79. package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
  80. package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
  81. package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
  82. package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
  83. package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
  84. package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
  85. package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
  86. package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
  87. package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
  88. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  89. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  90. package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
  91. package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
  92. package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
  93. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  94. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  95. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  96. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  97. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  98. package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
  99. package/dist/handlers-pspryh6g.js +41 -0
  100. package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
  101. package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
  102. package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
  103. package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
  104. package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
  105. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  106. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  107. package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
  108. package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
  109. package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
  110. package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
  111. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  112. package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
  113. package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
  114. package/dist/lead-c0pjcaw4.js +79 -0
  115. package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
  117. package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
  118. package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
  119. package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
  120. package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
  121. package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
  122. package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
  123. package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
  124. package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
  125. package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
  126. package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
  127. package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
  128. package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
  129. package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
  130. package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
  131. package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
  132. package/dist/worker-d4q9sqbc.js +79 -0
  133. package/openapi.json +1536 -407
  134. package/package.json +7 -5
  135. package/src/be/automation-preflight.ts +2 -0
  136. package/src/be/connector-codes.ts +65 -22
  137. package/src/be/db/api-keys.ts +9 -0
  138. package/src/be/db/workflow-runs.ts +36 -5
  139. package/src/be/db.ts +80 -22
  140. package/src/be/memory/boot-reembed.ts +4 -1
  141. package/src/be/memory/constants.ts +6 -0
  142. package/src/be/memory/embedding-recovery.ts +48 -0
  143. package/src/be/memory/index-content.ts +5 -2
  144. package/src/be/memory/providers/openai-embedding.ts +65 -14
  145. package/src/be/memory/task-learning.ts +257 -0
  146. package/src/be/memory/types.ts +13 -2
  147. package/src/be/migrations/245_script_executor.sql +3 -0
  148. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  149. package/src/be/oauth-refresh-sweep.ts +2 -1
  150. package/src/be/onboarding.ts +125 -15
  151. package/src/be/rbac-roles.ts +4 -1
  152. package/src/be/scripts/boot-reembed.ts +2 -0
  153. package/src/be/scripts/db.ts +14 -4
  154. package/src/be/scripts/embeddings.ts +4 -2
  155. package/src/be/scripts/run-saved.ts +1 -0
  156. package/src/be/scripts/typecheck.ts +9 -3
  157. package/src/be/seed/agent-fs-provision.ts +75 -14
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/swarm-config-guard.ts +3 -0
  160. package/src/cli.tsx +9 -0
  161. package/src/comb/config.ts +6 -1
  162. package/src/commands/onboard/agent-fs-version.ts +5 -0
  163. package/src/commands/onboard/compose-generator.ts +117 -4
  164. package/src/commands/onboard/env-generator.ts +9 -0
  165. package/src/commands/onboard/tei-image.ts +12 -0
  166. package/src/commands/onboard/types.ts +7 -0
  167. package/src/commands/onboard.tsx +11 -1
  168. package/src/commands/provider-credentials.ts +2 -1
  169. package/src/commands/runner.ts +14 -1
  170. package/src/heartbeat/heartbeat.ts +14 -1
  171. package/src/heartbeat/script-runner-probe.ts +47 -0
  172. package/src/http/agentfs-proxy.ts +369 -0
  173. package/src/http/all-routes.ts +1 -0
  174. package/src/http/api-keys.ts +24 -9
  175. package/src/http/codex-oauth-device.ts +8 -1
  176. package/src/http/fs.ts +64 -10
  177. package/src/http/idempotency-key.ts +1 -1
  178. package/src/http/inbox-state.ts +27 -1
  179. package/src/http/index.ts +7 -4
  180. package/src/http/memory.ts +7 -4
  181. package/src/http/onboarding.ts +139 -11
  182. package/src/http/pages-public.ts +38 -5
  183. package/src/http/scripts.ts +27 -0
  184. package/src/http/session-access.ts +24 -0
  185. package/src/http/sessions.ts +10 -6
  186. package/src/http/tasks.ts +85 -11
  187. package/src/http/telemetry-ui.ts +1 -1
  188. package/src/http/user-roles.ts +4 -1
  189. package/src/http/users.ts +272 -3
  190. package/src/http/utils.ts +22 -3
  191. package/src/http/workflows.ts +10 -0
  192. package/src/http/x.ts +5 -0
  193. package/src/prompts/base-prompt.ts +45 -10
  194. package/src/prompts/session-templates.ts +43 -5
  195. package/src/providers/amp-adapter.ts +1 -1
  196. package/src/providers/codex-adapter.ts +6 -1
  197. package/src/providers/cursor-adapter.ts +1 -1
  198. package/src/providers/dsh-adapter.ts +1 -1
  199. package/src/providers/pi-mono-adapter.ts +99 -14
  200. package/src/rbac/legacy-policy.ts +1 -0
  201. package/src/rbac/permissions.ts +4 -0
  202. package/src/scheduler/scheduler.ts +2 -1
  203. package/src/scripts-runtime/eval-harness.ts +8 -1
  204. package/src/scripts-runtime/executors/native.ts +22 -16
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  207. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  208. package/src/scripts-runtime/executors/registry.ts +32 -0
  209. package/src/scripts-runtime/executors/types.ts +7 -0
  210. package/src/scripts-runtime/host-validated-args.ts +133 -0
  211. package/src/scripts-runtime/loader.ts +30 -3
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +9 -1
  214. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
  215. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  216. package/src/tests/agentfs-proxy.test.ts +1106 -0
  217. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  218. package/src/tests/base-prompt.test.ts +139 -4
  219. package/src/tests/codex-adapter.test.ts +46 -8
  220. package/src/tests/codex-oauth-device.test.ts +12 -1
  221. package/src/tests/comb-config-guard.test.ts +20 -0
  222. package/src/tests/comb-links.test.ts +79 -1
  223. package/src/tests/comb-status.test.ts +44 -0
  224. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  225. package/src/tests/credential-check.test.ts +4 -2
  226. package/src/tests/docs-index-footprint.test.js +3 -3
  227. package/src/tests/docs-index.test.ts +135 -0
  228. package/src/tests/dsh-adapter.test.ts +1 -1
  229. package/src/tests/fetch-resolved-env.test.ts +16 -2
  230. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  231. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  232. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  233. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  234. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  235. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  236. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  237. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  238. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  239. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  240. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  241. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  242. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  243. package/src/tests/heartbeat-checklist.test.ts +89 -0
  244. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  245. package/src/tests/http-log-scrubbing.test.ts +34 -1
  246. package/src/tests/http-task-write-ownership.test.ts +5 -1
  247. package/src/tests/invites-http.test.ts +464 -0
  248. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  249. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  250. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  251. package/src/tests/onboard-compose.test.ts +162 -0
  252. package/src/tests/onboard-env.test.ts +24 -0
  253. package/src/tests/onboard-non-interactive.test.ts +29 -0
  254. package/src/tests/onboard-tei-image.test.ts +36 -0
  255. package/src/tests/onboarding.test.ts +451 -4
  256. package/src/tests/openai-embedding-provider.test.ts +105 -2
  257. package/src/tests/opencode-adapter.test.ts +2 -1
  258. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  259. package/src/tests/pi-codemode.test.ts +2 -1
  260. package/src/tests/pi-mono-adapter.test.ts +54 -0
  261. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  262. package/src/tests/pi-native-mcp.test.ts +13 -1
  263. package/src/tests/pi-tool-exposure.test.ts +8 -8
  264. package/src/tests/preload-env.test.ts +4 -0
  265. package/src/tests/preload.ts +4 -0
  266. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  267. package/src/tests/prompt-template-session.test.ts +20 -3
  268. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  269. package/src/tests/rbac-admission.test.ts +342 -4
  270. package/src/tests/rbac-engine.test.ts +1 -0
  271. package/src/tests/rbac-roles.test.ts +9 -1
  272. package/src/tests/request-composition-derive.test.ts +329 -0
  273. package/src/tests/script-connections.test.ts +2 -0
  274. package/src/tests/script-executor-conformance.test.ts +36 -1
  275. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  276. package/src/tests/script-executor-quickjs.test.ts +140 -0
  277. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  278. package/src/tests/script-per-script-executor.test.ts +415 -0
  279. package/src/tests/scripts-external-api.test.ts +45 -0
  280. package/src/tests/sdk-allowlist.test.ts +3 -2
  281. package/src/tests/self-improvement.test.ts +4 -4
  282. package/src/tests/send-task-requester-gate.test.ts +64 -3
  283. package/src/tests/sessions-move.test.ts +39 -1
  284. package/src/tests/store-progress-learning.test.ts +381 -0
  285. package/src/tests/ui-logs-parser.test.ts +180 -0
  286. package/src/tests/whoami-route.test.ts +41 -0
  287. package/src/tests/workflow-engine-v2.test.ts +116 -1
  288. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  289. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  290. package/src/tests/workflow-swarm-script.test.ts +34 -0
  291. package/src/tools/inject-learning.ts +1 -1
  292. package/src/tools/memory-edit.ts +1 -1
  293. package/src/tools/memory-search.ts +1 -1
  294. package/src/tools/script-common.ts +1 -0
  295. package/src/tools/script-run.ts +7 -0
  296. package/src/tools/script-upsert.ts +7 -0
  297. package/src/tools/store-progress.ts +71 -1
  298. package/src/tools/utils.ts +12 -1
  299. package/src/tools/workflows/trigger-workflow.ts +104 -10
  300. package/src/types.ts +12 -1
  301. package/src/utils/constants.ts +39 -0
  302. package/src/utils/credentials.ts +5 -2
  303. package/src/utils/task-attachment-links.ts +15 -3
  304. package/src/workflows/engine.ts +18 -16
  305. package/src/workflows/executors/swarm-script.ts +5 -0
  306. package/src/workflows/foreach-driver.ts +5 -11
  307. package/src/workflows/node-timeout.ts +30 -0
  308. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  309. package/templates/community/linear-delivery-loop/README.md +38 -0
  310. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  311. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  312. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  313. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  314. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  315. package/templates/skills/swarm-docs/config.json +14 -0
  316. package/templates/skills/swarm-docs/content.md +144 -0
  317. package/dist/handlers-eqsnmv95.js +0 -41
  318. package/dist/lead-23e7z59t.js +0 -79
  319. package/dist/worker-e3w7vye1.js +0 -79
  320. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  321. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  322. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  323. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  324. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
@@ -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
+ });
@@ -17,6 +17,7 @@ import {
17
17
  type ServerResponse,
18
18
  } from "node:http";
19
19
  import { closeDb, createUser, initDb, updateUser } from "../be/db";
20
+ import { ensureRbacSeedsSynced, setUserRoles } from "../be/rbac-roles";
20
21
  import { type IdentityActor, mintToken, revokeToken } from "../be/users";
21
22
  import { handleCore } from "../http/core";
22
23
  import { handleUsers } from "../http/users";
@@ -30,6 +31,7 @@ const ACTOR: IdentityActor = { kind: "operator", id: "op:test" };
30
31
  interface WhoamiBody {
31
32
  kind: "operator" | "user";
32
33
  user: { id: string; name: string; status: string } | null;
34
+ grants?: { grantsAll: boolean; verbs: string[]; roles: string[] };
33
35
  }
34
36
 
35
37
  function createTestServer(): Server {
@@ -61,6 +63,7 @@ describe("GET /api/whoami", () => {
61
63
  beforeAll(async () => {
62
64
  cleanupDb();
63
65
  initDb(TEST_DB_PATH);
66
+ ensureRbacSeedsSynced({ quiet: true });
64
67
  server = createTestServer();
65
68
  port = await listenOnFreePort(server);
66
69
  });
@@ -82,6 +85,7 @@ describe("GET /api/whoami", () => {
82
85
  const body = (await res.json()) as WhoamiBody;
83
86
  expect(body.kind).toBe("operator");
84
87
  expect(body.user).toBeNull();
88
+ expect(body.grants?.grantsAll).toBe(true);
85
89
  });
86
90
 
87
91
  test("active user token resolves to its bound user", async () => {
@@ -96,6 +100,43 @@ describe("GET /api/whoami", () => {
96
100
  expect(body.user?.name).toBe("Whoami User");
97
101
  });
98
102
 
103
+ test("requester token reports its role and verbs, without grantsAll", async () => {
104
+ const user = await createUser({ name: "Requester User", email: "req@example.com" });
105
+ await setUserRoles(user.id, ["requester"], null);
106
+ const { plaintext } = await mintToken(user.id, "rest", ACTOR);
107
+
108
+ const body = (await (await whoami(plaintext)).json()) as WhoamiBody;
109
+ expect(body.grants?.grantsAll).toBe(false);
110
+ expect(body.grants?.roles).toEqual(["requester"]);
111
+ expect(body.grants?.verbs).toContain("task.create.own");
112
+ expect(body.grants?.verbs).not.toContain("config.write.any");
113
+ });
114
+
115
+ test("a new user holds admin by default: grantsAll", async () => {
116
+ const user = await createUser({ name: "Default Admin" });
117
+ const { plaintext } = await mintToken(user.id, "rest", ACTOR);
118
+
119
+ const body = (await (await whoami(plaintext)).json()) as WhoamiBody;
120
+ expect(body.grants?.grantsAll).toBe(true);
121
+ expect(body.grants?.roles).toEqual(["admin"]);
122
+ });
123
+
124
+ test("RBAC_ENABLED=false reports grantsAll for a viewer", async () => {
125
+ const user = await createUser({ name: "Viewer User" });
126
+ await setUserRoles(user.id, ["viewer"], null);
127
+ const { plaintext } = await mintToken(user.id, "rest", ACTOR);
128
+ const previous = process.env.RBAC_ENABLED;
129
+ process.env.RBAC_ENABLED = "false";
130
+ try {
131
+ const body = (await (await whoami(plaintext)).json()) as WhoamiBody;
132
+ expect(body.grants?.grantsAll).toBe(true);
133
+ expect(body.grants?.roles).toEqual(["viewer"]);
134
+ } finally {
135
+ if (previous === undefined) delete process.env.RBAC_ENABLED;
136
+ else process.env.RBAC_ENABLED = previous;
137
+ }
138
+ });
139
+
99
140
  test("revoked token is rejected at auth (401)", async () => {
100
141
  const user = await createUser({ name: "Revoked User" });
101
142
  const { plaintext, tokenId } = await mintToken(user.id, "rest", ACTOR);
@@ -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();
@@ -10,6 +10,7 @@ import {
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import {
12
12
  closeDb,
13
+ countWorkflowRuns,
13
14
  createAgent,
14
15
  createTaskExtended,
15
16
  createUser,
@@ -20,10 +21,13 @@ import {
20
21
  getWorkflowRun,
21
22
  getWorkflowRunStepsByRunId,
22
23
  initDb,
24
+ updateWorkflow,
23
25
  } from "../be/db";
24
26
  import { getPathSegments, parseQueryParams } from "../http/utils";
25
27
  import { handleWorkflows } from "../http/workflows";
26
28
  import { SCRIPT_LONG_TIMEOUT_HINT_MS } from "../scripts-runtime/executors/types";
29
+ import { SwarmConfig } from "../scripts-runtime/swarm-config";
30
+ import { createSwarmSdk } from "../scripts-runtime/swarm-sdk";
27
31
  import { WORKFLOW_LONG_SCRIPT_TIMEOUT_NUDGE } from "../tools/utils";
28
32
  import { registerCreateWorkflowTool } from "../tools/workflows/create-workflow";
29
33
  import { registerPatchWorkflowTool } from "../tools/workflows/patch-workflow";
@@ -56,6 +60,8 @@ type ToolResult = {
56
60
  versionCreated?: number;
57
61
  runId?: string;
58
62
  skipped?: boolean;
63
+ replayed?: boolean;
64
+ idempotencyConflict?: boolean;
59
65
  validationErrors?: string[];
60
66
  triggerSchema?: Record<string, unknown>;
61
67
  };
@@ -627,6 +633,209 @@ describe("MCP create-workflow / update-workflow / patch-workflow accept triggerS
627
633
  expect((await getTaskByWorkflowRunStepId(step!.id))?.requestedByUserId).toBe(requester.id);
628
634
  });
629
635
 
636
+ test("trigger-workflow wait:false returns the runId before the run's instant steps finish", async () => {
637
+ const workflow = await createWorkflow({
638
+ name: uniqueName("mcp-trigger-detach"),
639
+ definition: {
640
+ nodes: [
641
+ {
642
+ id: "slow",
643
+ type: "script",
644
+ config: { runtime: "bash", script: "sleep 0.6; echo done", timeout: 10_000 },
645
+ },
646
+ ],
647
+ },
648
+ });
649
+ createdWorkflowIds.push(workflow.id);
650
+
651
+ const started = performance.now();
652
+ const triggered = await tools.callTrigger({ id: workflow.id, triggerData: {}, wait: false });
653
+ const elapsedMs = performance.now() - started;
654
+
655
+ expect(triggered.structuredContent?.success).toBe(true);
656
+ expect(triggered.structuredContent?.skipped).toBe(false);
657
+ const runId = triggered.structuredContent?.runId;
658
+ expect(runId).toBeString();
659
+ expect(elapsedMs).toBeLessThan(500);
660
+ expect((await getWorkflowRun(runId!))?.status).toBe("running");
661
+
662
+ const deadline = Date.now() + 10_000;
663
+ while ((await getWorkflowRun(runId!))?.status === "running" && Date.now() < deadline) {
664
+ await Bun.sleep(50);
665
+ }
666
+ expect((await getWorkflowRun(runId!))?.status).toBe("completed");
667
+ }, 15_000);
668
+
669
+ // A caller killed after its trigger committed (or while its request was in
670
+ // flight) retries with the same key: the retry must return that run and
671
+ // start no second one. Before idempotencyKey, the only guard was a separate
672
+ // claim the caller wrote first, which a kill between the two left held.
673
+ test("trigger-workflow idempotencyKey: a retry replays the accepted run and starts no second one", async () => {
674
+ const workflow = await createWorkflow({
675
+ name: uniqueName("mcp-trigger-idempotent"),
676
+ definition: { nodes: [{ id: "n", type: "property-match", config: { conditions: [] } }] },
677
+ });
678
+ createdWorkflowIds.push(workflow.id);
679
+ const agentId = crypto.randomUUID();
680
+ const args = {
681
+ id: workflow.id,
682
+ triggerData: { pr: 7, sha: "a".repeat(40) },
683
+ wait: false,
684
+ idempotencyKey: "pr-review/repo/pr/7/aaaa",
685
+ };
686
+
687
+ const first = await tools.callTrigger(args, agentId);
688
+ expect(first.structuredContent?.success).toBe(true);
689
+ const runId = first.structuredContent?.runId;
690
+ expect(runId).toBeString();
691
+ expect(first.structuredContent?.replayed).toBeUndefined();
692
+
693
+ // The caller never saw `first` (killed): its retry replays it.
694
+ const retry = await tools.callTrigger(args, agentId);
695
+ expect(retry.structuredContent?.success).toBe(true);
696
+ expect(retry.structuredContent?.runId).toBe(runId);
697
+ expect(retry.structuredContent?.replayed).toBe(true);
698
+
699
+ // The same key with other data starts nothing either.
700
+ const conflict = await tools.callTrigger(
701
+ { ...args, triggerData: { pr: 7, sha: "b".repeat(40) } },
702
+ agentId,
703
+ );
704
+ expect(conflict.structuredContent?.success).toBe(false);
705
+ expect(conflict.structuredContent?.idempotencyConflict).toBe(true);
706
+
707
+ // The key is scoped to the caller: another agent's key does not collide.
708
+ const other = await tools.callTrigger(args, crypto.randomUUID());
709
+ expect(other.structuredContent?.runId).not.toBe(runId);
710
+
711
+ expect(await countWorkflowRuns(workflow.id)).toBe(2);
712
+ });
713
+
714
+ test("trigger-workflow idempotencyKey: concurrent triggers with one key start exactly one run", async () => {
715
+ const workflow = await createWorkflow({
716
+ name: uniqueName("mcp-trigger-idempotent-race"),
717
+ definition: { nodes: [{ id: "n", type: "property-match", config: { conditions: [] } }] },
718
+ });
719
+ createdWorkflowIds.push(workflow.id);
720
+ const agentId = crypto.randomUUID();
721
+ const args = { id: workflow.id, triggerData: { pr: 8 }, idempotencyKey: "race-key" };
722
+
723
+ const results = await Promise.all(
724
+ Array.from({ length: 6 }, () => tools.callTrigger(args, agentId)),
725
+ );
726
+ const runIds = new Set(results.map((r) => r.structuredContent?.runId));
727
+ expect(results.every((r) => r.structuredContent?.success === true)).toBe(true);
728
+ expect(runIds.size).toBe(1);
729
+ expect(results.filter((r) => r.structuredContent?.replayed !== true)).toHaveLength(1);
730
+ expect(await countWorkflowRuns(workflow.id)).toBe(1);
731
+ });
732
+
733
+ test("trigger-workflow idempotencyKey: a key rejected before acceptance is not used up", async () => {
734
+ const workflow = await createWorkflow({
735
+ name: uniqueName("mcp-trigger-idempotent-disabled"),
736
+ definition: { nodes: [{ id: "n", type: "property-match", config: { conditions: [] } }] },
737
+ enabled: false,
738
+ });
739
+ createdWorkflowIds.push(workflow.id);
740
+ const agentId = crypto.randomUUID();
741
+ const args = { id: workflow.id, triggerData: {}, idempotencyKey: "after-enable" };
742
+
743
+ expect((await tools.callTrigger(args, agentId)).structuredContent?.success).toBe(false);
744
+ expect(await countWorkflowRuns(workflow.id)).toBe(0);
745
+
746
+ await updateWorkflow(workflow.id, { enabled: true });
747
+ const accepted = await tools.callTrigger(args, agentId);
748
+ expect(accepted.structuredContent?.success).toBe(true);
749
+ expect(accepted.structuredContent?.replayed).toBeUndefined();
750
+ expect(await countWorkflowRuns(workflow.id)).toBe(1);
751
+ });
752
+
753
+ // `ctx.swarm.workflow_trigger` in a script calls the REST trigger route, not
754
+ // the MCP tool, so `wait` and `idempotencyKey` must reach that route too.
755
+ const scriptSdk = (agentId: string) =>
756
+ createSwarmSdk(
757
+ new SwarmConfig({
758
+ system: {
759
+ apiKey: { value: "sdk-test-key", isSecret: true },
760
+ agentId: { value: agentId, isSecret: false },
761
+ mcpBaseUrl: { value: `http://127.0.0.1:${TEST_PORT}`, isSecret: false },
762
+ },
763
+ user: {},
764
+ }),
765
+ );
766
+ type SdkTriggerResult = { success: boolean; status: number; data: { runId?: string } };
767
+
768
+ test("ctx.swarm.workflow_trigger idempotencyKey: a retry after a kill replays the run", async () => {
769
+ const workflow = await createWorkflow({
770
+ name: uniqueName("sdk-trigger-idempotent"),
771
+ definition: { nodes: [{ id: "n", type: "property-match", config: { conditions: [] } }] },
772
+ });
773
+ createdWorkflowIds.push(workflow.id);
774
+ const sdk = scriptSdk(crypto.randomUUID());
775
+ const args = {
776
+ id: workflow.id,
777
+ triggerData: { pr: 9 },
778
+ wait: false,
779
+ idempotencyKey: "pr-review/repo/pr/9/sha",
780
+ };
781
+
782
+ const first = (await sdk.workflow_trigger(args)) as SdkTriggerResult;
783
+ const retry = (await sdk.workflow_trigger(args)) as SdkTriggerResult;
784
+ expect(first.success).toBe(true);
785
+ expect(first.data.runId).toBeString();
786
+ expect(retry.success).toBe(true);
787
+ expect(retry.data.runId).toBe(first.data.runId);
788
+ expect(await countWorkflowRuns(workflow.id)).toBe(1);
789
+
790
+ // Two concurrent calls (a killed attempt's in-flight request and its retry).
791
+ const racing = await createWorkflow({
792
+ name: uniqueName("sdk-trigger-idempotent-race"),
793
+ definition: { nodes: [{ id: "n", type: "property-match", config: { conditions: [] } }] },
794
+ });
795
+ createdWorkflowIds.push(racing.id);
796
+ const raced = (await Promise.all(
797
+ Array.from({ length: 4 }, () => sdk.workflow_trigger({ ...args, id: racing.id })),
798
+ )) as SdkTriggerResult[];
799
+ expect(new Set(raced.map((r) => r.data.runId)).size).toBe(1);
800
+ expect(await countWorkflowRuns(racing.id)).toBe(1);
801
+ });
802
+
803
+ test("ctx.swarm.workflow_trigger wait:false returns before the run's instant steps finish", async () => {
804
+ const workflow = await createWorkflow({
805
+ name: uniqueName("sdk-trigger-detach"),
806
+ definition: {
807
+ nodes: [
808
+ {
809
+ id: "slow",
810
+ type: "script",
811
+ config: { runtime: "bash", script: "sleep 0.6; echo done", timeout: 10_000 },
812
+ },
813
+ ],
814
+ },
815
+ });
816
+ createdWorkflowIds.push(workflow.id);
817
+
818
+ const started = performance.now();
819
+ const triggered = (await scriptSdk(crypto.randomUUID()).workflow_trigger({
820
+ id: workflow.id,
821
+ triggerData: {},
822
+ wait: false,
823
+ })) as SdkTriggerResult;
824
+ const elapsedMs = performance.now() - started;
825
+
826
+ expect(triggered.success).toBe(true);
827
+ const runId = triggered.data.runId;
828
+ expect(runId).toBeString();
829
+ expect(elapsedMs).toBeLessThan(500);
830
+ expect((await getWorkflowRun(runId!))?.status).toBe("running");
831
+
832
+ const deadline = Date.now() + 10_000;
833
+ while ((await getWorkflowRun(runId!))?.status === "running" && Date.now() < deadline) {
834
+ await Bun.sleep(50);
835
+ }
836
+ expect((await getWorkflowRun(runId!))?.status).toBe("completed");
837
+ }, 15_000);
838
+
630
839
  test("trigger-workflow with missing required field → structured TriggerSchemaError", async () => {
631
840
  const triggerSchema: Record<string, unknown> = {
632
841
  type: "object",