@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
@@ -2,10 +2,42 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { resolveTaskAuditUserId } from "@/be/audit-user";
4
4
  import { getWorkflow, getWorkflowRun } from "@/be/db";
5
+ import {
6
+ findAcceptedWorkflowOperation,
7
+ IdempotencyKeyTakenError,
8
+ recordAcceptedWorkflowOperation,
9
+ type WorkflowIdempotencyClaim,
10
+ workflowRequestFingerprint,
11
+ } from "@/be/db/workflow-idempotency";
12
+ import { IDEMPOTENCY_KEY_PATTERN } from "@/http/idempotency-key";
5
13
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
6
14
  import { getExecutorRegistry, startWorkflowExecution } from "@/workflows";
7
15
  import { TriggerSchemaError } from "@/workflows/engine";
8
16
 
17
+ /**
18
+ * Answer from the run already accepted under this key, or null when there is
19
+ * none. Same record and rules as the HTTP trigger's `Idempotency-Key`.
20
+ */
21
+ async function replayAcceptedTrigger(claim: WorkflowIdempotencyClaim) {
22
+ const accepted = await findAcceptedWorkflowOperation(claim);
23
+ if (!accepted) return null;
24
+ if (accepted.fingerprint !== claim.fingerprint) {
25
+ return toolErr("idempotencyKey was already used to trigger this workflow with other data.", {
26
+ data: { idempotencyConflict: true },
27
+ });
28
+ }
29
+ if (accepted.resourceDeletedAt !== null) {
30
+ return toolErr(
31
+ "The workflow this idempotencyKey triggered was deleted; send a new key to start over.",
32
+ );
33
+ }
34
+ const { runId, skipped } = accepted.body as { runId: string; skipped: boolean };
35
+ return toolOk("Run already accepted under this idempotencyKey; nothing new was started.", {
36
+ details: `Replayed the run accepted under this idempotencyKey — run ID: ${runId}.`,
37
+ data: { runId, skipped, replayed: true },
38
+ });
39
+ }
40
+
9
41
  export const registerTriggerWorkflowTool = (server: McpServer) => {
10
42
  createToolRegistrar(server)(
11
43
  "trigger-workflow",
@@ -21,16 +53,55 @@ export const registerTriggerWorkflowTool = (server: McpServer) => {
21
53
  .record(z.string(), z.unknown())
22
54
  .optional()
23
55
  .describe("Optional data to pass as trigger context to the workflow"),
56
+ wait: z
57
+ .boolean()
58
+ .optional()
59
+ .describe(
60
+ "Default true: return after the run's instant steps ran, when it finishes or parks at its first async step. " +
61
+ "false: return as soon as the run is created and walk it in the background. Use false from a script or step that only needs the runId.",
62
+ ),
63
+ idempotencyKey: z
64
+ .string()
65
+ .regex(IDEMPOTENCY_KEY_PATTERN)
66
+ .optional()
67
+ .describe(
68
+ "Optional, 1-255 visible ASCII characters, scoped to the calling agent and this workflow. " +
69
+ "The first trigger accepted under a key starts one run; the key is recorded in the run's own transaction and never expires. " +
70
+ "A retry with the same key and triggerData starts nothing and returns that run (replayed: true), even if the earlier call was killed before it saw the answer. " +
71
+ "The same key with other triggerData is an error with idempotencyConflict: true.",
72
+ ),
24
73
  }),
25
74
  outputSchema: swarmToolOutputSchema({
26
75
  runId: z.string().optional(),
27
76
  skipped: z.boolean().optional(),
77
+ replayed: z.boolean().optional(),
78
+ idempotencyConflict: z.boolean().optional(),
28
79
  validationErrors: z.array(z.string()).optional(),
29
80
  triggerSchema: z.record(z.string(), z.unknown()).optional(),
30
81
  }),
31
82
  },
32
- async ({ id, triggerData }, requestInfo) => {
83
+ async ({ id, triggerData, wait, idempotencyKey }, requestInfo) => {
33
84
  try {
85
+ if (idempotencyKey !== undefined && !requestInfo.agentId) {
86
+ return toolErr("idempotencyKey needs an identified caller (X-Agent-ID).");
87
+ }
88
+ const claim: WorkflowIdempotencyClaim | undefined =
89
+ idempotencyKey === undefined
90
+ ? undefined
91
+ : {
92
+ principal: `agent:${requestInfo.agentId}`,
93
+ key: idempotencyKey,
94
+ operation: "workflow.trigger",
95
+ targetId: id,
96
+ fingerprint: workflowRequestFingerprint("workflow.trigger", triggerData ?? {}),
97
+ };
98
+ if (claim) {
99
+ // Before the enabled, cooldown and trigger-schema checks: an
100
+ // accepted trigger replays even after the workflow changed.
101
+ const replayed = await replayAcceptedTrigger(claim);
102
+ if (replayed) return replayed;
103
+ }
104
+
34
105
  const workflow = await getWorkflow(id);
35
106
  if (!workflow) {
36
107
  return toolErr(`Workflow not found: ${id}`);
@@ -38,21 +109,44 @@ export const registerTriggerWorkflowTool = (server: McpServer) => {
38
109
  if (!workflow.enabled) {
39
110
  return toolErr(`Workflow "${workflow.name}" is disabled.`);
40
111
  }
41
- const runId = await startWorkflowExecution(
42
- workflow,
43
- triggerData ?? {},
44
- getExecutorRegistry(),
45
- {
112
+ let accepted: { runId: string; skipped: boolean } | undefined;
113
+ let runId: string;
114
+ try {
115
+ runId = await startWorkflowExecution(workflow, triggerData ?? {}, getExecutorRegistry(), {
46
116
  triggerType: "manual",
47
117
  requestedByUserId:
48
118
  (await resolveTaskAuditUserId(requestInfo.sourceTaskId, requestInfo.agentId)) ??
49
119
  undefined,
50
- },
51
- );
120
+ detach: wait === false,
121
+ // Recorded in the run's own transaction: the key exists if and
122
+ // only if the run committed.
123
+ accept: claim
124
+ ? async (run) => {
125
+ const response = { runId: run.runId, skipped: run.skipped };
126
+ await recordAcceptedWorkflowOperation(claim, {
127
+ workflowId: workflow.id,
128
+ runId: run.runId,
129
+ status: 201,
130
+ body: response,
131
+ });
132
+ accepted = response;
133
+ }
134
+ : undefined,
135
+ });
136
+ } catch (err) {
137
+ // A concurrent call with this key committed its run first; this one
138
+ // created none.
139
+ if (claim && err instanceof IdempotencyKeyTakenError) {
140
+ const replayed = await replayAcceptedTrigger(claim);
141
+ if (replayed) return replayed;
142
+ }
143
+ throw err;
144
+ }
52
145
 
53
146
  // Check if the run was skipped due to cooldown
54
- const run = await getWorkflowRun(runId);
55
- const skipped = run?.status === "skipped";
147
+ const skipped = accepted
148
+ ? accepted.skipped
149
+ : (await getWorkflowRun(runId))?.status === "skipped";
56
150
 
57
151
  if (skipped) {
58
152
  return toolOk(`Workflow "${workflow.name}" skipped (cooldown).`, {
package/src/types.ts CHANGED
@@ -2282,7 +2282,14 @@ export const ExecutorMetaSchema = z
2282
2282
  inputCtx: z.record(z.string(), z.unknown()).optional(),
2283
2283
  })
2284
2284
  .openapi("ExecutorMeta");
2285
- export type ExecutorMeta = z.infer<typeof ExecutorMetaSchema>;
2285
+ export type ExecutorMeta = z.infer<typeof ExecutorMetaSchema> & {
2286
+ /**
2287
+ * Aborted when the step's watchdog budget runs out (`startStepDeadline`).
2288
+ * Runtime-only, never serialized. An executor with a long or one-shot side
2289
+ * effect should forward it so a timed-out step stops instead of finishing.
2290
+ */
2291
+ signal?: AbortSignal;
2292
+ };
2286
2293
 
2287
2294
  // --- Validation ---
2288
2295
 
@@ -3203,6 +3210,9 @@ export type ScriptScope = z.infer<typeof ScriptScopeSchema>;
3203
3210
  export const ScriptFsModeSchema = z.enum(["none", "workspace-rw"]);
3204
3211
  export type ScriptFsMode = z.infer<typeof ScriptFsModeSchema>;
3205
3212
 
3213
+ export const ScriptExecutorNameSchema = z.enum(["native", "quickjs"]);
3214
+ export type ScriptExecutorName = z.infer<typeof ScriptExecutorNameSchema>;
3215
+
3206
3216
  export const ScriptRecordSchema = z
3207
3217
  .object({
3208
3218
  id: z.string(),
@@ -3219,6 +3229,7 @@ export const ScriptRecordSchema = z
3219
3229
  isScratch: z.boolean(),
3220
3230
  typeChecked: z.boolean(),
3221
3231
  fsMode: ScriptFsModeSchema,
3232
+ executor: ScriptExecutorNameSchema.nullable().default(null),
3222
3233
  createdByAgentId: z.string().nullable(),
3223
3234
  createdAt: z.string(),
3224
3235
  updatedAt: z.string(),
@@ -89,6 +89,36 @@ export function getAgentFsLiveUrl(): string {
89
89
  return (raw || DEFAULT_AGENT_FS_LIVE_URL).replace(/\/+$/, "");
90
90
  }
91
91
 
92
+ /** Where people-facing agent-fs file links point: Comb in the dashboard, or the live app. */
93
+ export type AgentFsLinkTarget = "dashboard" | "live";
94
+
95
+ /**
96
+ * The operator's `AGENT_FS_LINK_TARGET` choice (trimmed, lowercased). Null when
97
+ * unset, blank, or invalid: every surface then keeps its automatic behavior.
98
+ */
99
+ export function readAgentFsLinkTarget(): AgentFsLinkTarget | null {
100
+ const raw = process.env.AGENT_FS_LINK_TARGET?.trim().toLowerCase();
101
+ return raw === "dashboard" || raw === "live" ? raw : null;
102
+ }
103
+
104
+ /**
105
+ * The base URL (no trailing slash) that a people-facing file link starts with,
106
+ * per {@link readAgentFsLinkTarget}. `dashboard` uses `APP_URL` when one is
107
+ * configured (and, for attachments, Comb is on), else the live app. Null when
108
+ * no target is set: the caller keeps its current code path.
109
+ *
110
+ * The prompt surface skips the Comb check: workers do not get `COMB_ENABLED`,
111
+ * and an explicit `dashboard` is the operator's choice.
112
+ */
113
+ export function resolveAgentFsLinkBase(surface: "prompt" | "attachment"): string | null {
114
+ const target = readAgentFsLinkTarget();
115
+ if (target === null) return null;
116
+ if (target === "dashboard" && getConfiguredAppUrls().length > 0) {
117
+ if (surface === "prompt" || isCombEnabled()) return getAppUrl();
118
+ }
119
+ return getAgentFsLiveUrl();
120
+ }
121
+
92
122
  /**
93
123
  * Optional fallback agent-fs `org_id` for attachments that store only `path`.
94
124
  * Strictly opt-in — when neither env var is set, the renderer keeps the
@@ -166,6 +196,15 @@ export function isCombEnabled(): boolean {
166
196
  return isEnvFlagEnabled("COMB_ENABLED", false) && Boolean(apiUrl);
167
197
  }
168
198
 
199
+ /**
200
+ * `AGENT_FS_PROXY`: the API serves agent-fs at `/agentfs/*` (plus the public
201
+ * `/share/*` and `/site/*` pages), and Comb uses `<public API URL>/agentfs`.
202
+ * Read per request, so a config upsert applies without a restart. Off by default.
203
+ */
204
+ export function isAgentFsProxyEnabled(): boolean {
205
+ return isEnvFlagEnabled("AGENT_FS_PROXY", false);
206
+ }
207
+
169
208
  /**
170
209
  * The dashboard (Comb) URL for an agent-fs file, same id and encoding rules as
171
210
  * {@link buildAgentFsLiveUrl}. Null unless Comb is on AND an explicit
@@ -406,10 +406,13 @@ async function fetchAvailableIndices(
406
406
  for (const envVar of poolVars) {
407
407
  const val = env[envVar];
408
408
  if (val) {
409
- const totalKeys = val.includes(",") ? val.split(",").filter((s) => s.trim()).length : 1;
409
+ // Status is matched by key suffix, so a key at the same index in another
410
+ // agent's or repo's pool never blocks one of ours (issue #2425).
411
+ const keySuffixes = splitCredentialPool(val).map((key) => key.slice(-5));
412
+ const totalKeys = keySuffixes.length;
410
413
  try {
411
414
  const resp = await fetch(
412
- `${apiUrl}/api/keys/available?keyType=${encodeURIComponent(envVar)}&totalKeys=${totalKeys}${modelParam}`,
415
+ `${apiUrl}/api/keys/available?keyType=${encodeURIComponent(envVar)}&totalKeys=${totalKeys}${modelParam}&keySuffixes=${encodeURIComponent(keySuffixes.join(","))}`,
413
416
  { headers: { Authorization: `Bearer ${apiKey}` } },
414
417
  );
415
418
  if (resp.ok) {
@@ -1,5 +1,11 @@
1
1
  import type { TaskAttachment } from "../types";
2
- import { buildAgentFsLiveUrl, buildCombFileUrl, getAppUrl } from "./constants";
2
+ import {
3
+ agentFsFileRoute,
4
+ buildAgentFsLiveUrl,
5
+ buildCombFileUrl,
6
+ getAppUrl,
7
+ resolveAgentFsLinkBase,
8
+ } from "./constants";
3
9
 
4
10
  function shellQuote(value: string): string {
5
11
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -26,8 +32,9 @@ export function taskAttachmentFetchCommand(
26
32
  /**
27
33
  * The link a human (Slack, prompts, citations) sees for an attachment. An
28
34
  * agent-fs file links to the dashboard (Comb) when Comb is on and `APP_URL` is
29
- * set, else to the agent-fs live UI, else to the raw `agent-fs:<path>`.
30
- * `comb: false` skips the dashboard link.
35
+ * set, else to the agent-fs live UI, else to the raw `agent-fs:<path>`. An
36
+ * explicit `AGENT_FS_LINK_TARGET` picks the base instead (see
37
+ * `resolveAgentFsLinkBase`). `comb: false` always links to the live UI.
31
38
  */
32
39
  export function taskAttachmentDisplayUrl(
33
40
  attachment: TaskAttachment,
@@ -40,6 +47,11 @@ export function taskAttachmentDisplayUrl(
40
47
 
41
48
  if (attachment.providerId === "agent-fs" || attachment.kind === "agent-fs") {
42
49
  const file = { path: attachment.path, orgId: attachment.orgId, driveId: attachment.driveId };
50
+ const linkBase = opts.comb === false ? null : resolveAgentFsLinkBase("attachment");
51
+ if (linkBase !== null) {
52
+ const route = agentFsFileRoute(file);
53
+ return route ? `${linkBase}/file/~/${route}` : `agent-fs:${attachment.path ?? ""}`;
54
+ }
43
55
  const combUrl = opts.comb === false ? null : buildCombFileUrl(file);
44
56
  return combUrl ?? buildAgentFsLiveUrl(file) ?? `agent-fs:${attachment.path ?? ""}`;
45
57
  }
@@ -51,7 +51,7 @@ import { FOREACH_TERMINAL_STEP_STATUSES, resolveForeachParent } from "./foreach-
51
51
  import { getSecretInputKeys, redactSecretsForStorage, resolveInputs } from "./input";
52
52
  import { validateJsonSchema } from "./json-schema-validator";
53
53
  import { getMaxWorkflowStepsPerRun } from "./limits";
54
- import { resolveNodeTimeoutMs } from "./node-timeout";
54
+ import { resolveNodeTimeoutMs, startStepDeadline } from "./node-timeout";
55
55
  import { findWorkflowReadinessProblems, formatReadinessRunError } from "./readiness";
56
56
  import { deepInterpolate } from "./template";
57
57
  import { runStepValidation, type ValidationRunResult } from "./validation";
@@ -78,6 +78,13 @@ export interface WorkflowExecutionOptions {
78
78
  * HTTP trigger records its `Idempotency-Key` here.
79
79
  */
80
80
  accept?: (run: { runId: string; skipped: boolean }) => Promise<void>;
81
+ /**
82
+ * Return once the run is committed and walk it in the background, as a
83
+ * child run does. By default a top-level start resolves only after the
84
+ * walk reaches its end or its first async step, so a caller that only
85
+ * needs the run id waits for every instant step before it.
86
+ */
87
+ detach?: boolean;
81
88
  }
82
89
 
83
90
  /**
@@ -209,9 +216,12 @@ export async function startWorkflowExecution(
209
216
  options,
210
217
  );
211
218
  // A child run walks on its own, outside the parent executor's timeout. Its
212
- // terminal status wakes the parent step (`resumeFromChildRun`).
213
- if (options.parentStepId) {
214
- walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
219
+ // terminal status wakes the parent step (`resumeFromChildRun`). A detached
220
+ // top-level run walks the same way; the walk holds the run before its
221
+ // first await, so releasing the start hold below leaves no gap.
222
+ if (options.parentStepId || options.detach) {
223
+ const kind = options.parentStepId ? "Child" : "Detached";
224
+ walk.catch((err) => console.error(`[workflows] ${kind} workflow run ${runId} failed:`, err));
215
225
  return runId;
216
226
  }
217
227
  await walk;
@@ -1045,7 +1055,7 @@ async function runClaimedStep(
1045
1055
  inputCtx: interpolationCtx,
1046
1056
  };
1047
1057
 
1048
- const timeoutMs = resolveNodeTimeoutMs(node);
1058
+ const deadline = startStepDeadline(resolveNodeTimeoutMs(node));
1049
1059
 
1050
1060
  let result: Awaited<ReturnType<typeof executor.run>>;
1051
1061
  try {
@@ -1053,10 +1063,10 @@ async function runClaimedStep(
1053
1063
  executor.run({
1054
1064
  config: interpolatedConfig,
1055
1065
  context: executionCtx,
1056
- meta,
1066
+ meta: { ...meta, signal: deadline.signal },
1057
1067
  }),
1058
- timeoutPromise(timeoutMs),
1059
- ]);
1068
+ deadline.expired,
1069
+ ]).finally(deadline.clear);
1060
1070
  } catch (err) {
1061
1071
  const errorMsg = err instanceof Error ? err.message : String(err);
1062
1072
 
@@ -1459,11 +1469,3 @@ export function scriptBodyInterpolationError(nodeId: string, tokens: string[]):
1459
1469
  "Pass dynamic trigger or upstream values through config.args and read them from argv."
1460
1470
  );
1461
1471
  }
1462
-
1463
- // ─── Helpers ───────────────────────────────────────────────
1464
-
1465
- function timeoutPromise(ms: number): Promise<never> {
1466
- return new Promise((_, reject) => {
1467
- setTimeout(() => reject(new Error(`Step timed out after ${ms}ms`)), ms);
1468
- });
1469
- }
@@ -123,6 +123,7 @@ export class SwarmScriptExecutor extends BaseExecutor<
123
123
  source: resolved.source,
124
124
  args: config.args,
125
125
  fsMode: "none",
126
+ executor: resolved.script.executor,
126
127
  agentId: agentId ?? "workflow",
127
128
  mcpBaseUrl: getMcpBaseUrl(),
128
129
  egressSecrets: credentials.egressSecrets,
@@ -132,6 +133,10 @@ export class SwarmScriptExecutor extends BaseExecutor<
132
133
  apiConnections: getScriptApiConnectionDescriptors({ agentId: agentId ?? undefined }),
133
134
  mcpConnections: getScriptMcpConnectionDescriptors({ agentId: agentId ?? undefined }),
134
135
  timeoutMs: config.timeoutMs,
136
+ // The step watchdog aborts this when the node budget runs out, which
137
+ // kills the script process: a step reported as timed out must not go
138
+ // on to perform the script's side effects.
139
+ signal: meta.signal,
135
140
  });
136
141
  if (output.exitCode === 0 && !output.error && !output.runtimeError) {
137
142
  await touchScratchScriptLastUsed(resolved.script.id);
@@ -47,7 +47,7 @@ import {
47
47
  settleForeachChild,
48
48
  } from "./foreach-join";
49
49
  import { getSecretInputKeys } from "./input";
50
- import { resolveNodeTimeoutMs } from "./node-timeout";
50
+ import { resolveNodeTimeoutMs, startStepDeadline } from "./node-timeout";
51
51
  import { rebuildNodeExecutionInput } from "./step-input";
52
52
 
53
53
  export { FOREACH_ITEMS_WAIT_FOR };
@@ -480,27 +480,21 @@ async function startChild(input: {
480
480
  }
481
481
 
482
482
  const executor = registry.get(bodyType);
483
- const timeoutMs = resolveNodeTimeoutMs({ type: bodyType, config });
484
- let timer: ReturnType<typeof setTimeout> | undefined;
483
+ const deadline = startStepDeadline(resolveNodeTimeoutMs({ type: bodyType, config }));
485
484
  let result: Awaited<ReturnType<typeof executor.run>>;
486
485
  try {
487
486
  result = await Promise.race([
488
487
  executor.run({
489
488
  config,
490
489
  context: { ...ctx, ...bodyCtx },
491
- meta: { ...meta, stepId: child.id, nodeId: child.nodeId },
492
- }),
493
- new Promise<never>((_, reject) => {
494
- timer = setTimeout(
495
- () => reject(new Error(`Step timed out after ${timeoutMs}ms`)),
496
- timeoutMs,
497
- );
490
+ meta: { ...meta, stepId: child.id, nodeId: child.nodeId, signal: deadline.signal },
498
491
  }),
492
+ deadline.expired,
499
493
  ]);
500
494
  } catch (err) {
501
495
  return { status: "failed", error: err instanceof Error ? err.message : String(err) };
502
496
  } finally {
503
- if (timer) clearTimeout(timer);
497
+ deadline.clear();
504
498
  }
505
499
 
506
500
  if (result.status === "failed") {
@@ -19,3 +19,33 @@ export function resolveNodeTimeoutMs(node: Pick<WorkflowNode, "type" | "config">
19
19
  if (node.type === "script" && typeof config.timeout === "number") return config.timeout;
20
20
  return DEFAULT_NODE_TIMEOUT_MS;
21
21
  }
22
+
23
+ export interface StepDeadline {
24
+ /** Aborted when the budget runs out. The executor gets it as `meta.signal`. */
25
+ signal: AbortSignal;
26
+ /** Rejects with "Step timed out after <ms>ms" when the budget runs out. */
27
+ expired: Promise<never>;
28
+ /** Stop the timer once the executor call settled. */
29
+ clear: () => void;
30
+ }
31
+
32
+ /**
33
+ * The watchdog for one executor call. Racing `expired` alone settles the step
34
+ * but leaves the executor running, so a step reported as timed out could still
35
+ * perform its side effects. On expiry the deadline also aborts `signal`, so an
36
+ * executor that forwards it (swarm-script kills its script process) stops.
37
+ */
38
+ export function startStepDeadline(timeoutMs: number): StepDeadline {
39
+ const controller = new AbortController();
40
+ let timer: ReturnType<typeof setTimeout> | undefined;
41
+ const expired = new Promise<never>((_, reject) => {
42
+ timer = setTimeout(() => {
43
+ const error = new Error(`Step timed out after ${timeoutMs}ms`);
44
+ // Reject first so the race settles on the timeout, not on the
45
+ // executor's "killed" result that the abort produces.
46
+ reject(error);
47
+ controller.abort(error);
48
+ }, timeoutMs);
49
+ });
50
+ return { signal: controller.signal, expired, clear: () => clearTimeout(timer) };
51
+ }