@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
@@ -25,6 +25,7 @@ import { registerBunOAuthFlows } from "@earendil-works/pi-ai/bun-oauth";
25
25
  import { getEnvApiKey } from "@earendil-works/pi-ai/compat";
26
26
  import { builtinModels } from "@earendil-works/pi-ai/providers/all";
27
27
  import { getValidCodexOAuth, persistCodexOAuth } from "../../providers/codex-oauth/storage.js";
28
+ import { selectCredential } from "../credentials.js";
28
29
  import { type CredentialKind, DEFAULT_MODEL, resolveRaterModelString } from "./models.js";
29
30
 
30
31
  registerBunOAuthFlows();
@@ -135,7 +136,7 @@ export async function resolveCredential(
135
136
  if (openrouterKey) {
136
137
  return {
137
138
  kind: "openrouter",
138
- apiKey: openrouterKey,
139
+ apiKey: selectCredential(openrouterKey, undefined, "OPENROUTER_API_KEY").selected,
139
140
  modelDefault: resolveRaterModelString("openrouter"),
140
141
  };
141
142
  }
@@ -151,7 +152,7 @@ export async function resolveCredential(
151
152
  if (anthropicKey) {
152
153
  return {
153
154
  kind: "anthropic",
154
- apiKey: anthropicKey,
155
+ apiKey: selectCredential(anthropicKey, undefined, "ANTHROPIC_API_KEY").selected,
155
156
  modelDefault: resolveRaterModelString("anthropic"),
156
157
  };
157
158
  }
@@ -161,7 +162,7 @@ export async function resolveCredential(
161
162
  if (openaiKey) {
162
163
  return {
163
164
  kind: "openai",
164
- apiKey: openaiKey,
165
+ apiKey: selectCredential(openaiKey, undefined, "OPENAI_API_KEY").selected,
165
166
  modelDefault: resolveRaterModelString("openai"),
166
167
  };
167
168
  }
@@ -0,0 +1,328 @@
1
+ /** Shared credential probes. This module does not import worker harnesses. */
2
+ import {
3
+ type OpenAICompatibleProvider,
4
+ providerBaseUrl,
5
+ providerForKeyVar,
6
+ } from "@desplega/model-catalog";
7
+ import {
8
+ createScopedFetch,
9
+ deriveDefaultRoute,
10
+ getRouteProvider,
11
+ type ModelRoute,
12
+ } from "@desplega/model-routing";
13
+ import { assertUrlSafe, publicEndpointSsrfOptions } from "../oauth/mcp-wrapper";
14
+ import { getOpenRouterAttributionHeaders, getOpenRouterBaseUrl } from "./openrouter-base-url";
15
+ import { scrubSecrets } from "./secret-scrubber";
16
+
17
+ interface ProbeOptions {
18
+ env?: Record<string, string | undefined>;
19
+ publicEndpoint?: boolean;
20
+ }
21
+
22
+ const LIVE_TEST_TIMEOUT_MS = 5_000;
23
+
24
+ function scrubProbeBody(body: string, init: RequestInit): string {
25
+ const headers = new Headers(init.headers);
26
+ const apiKey =
27
+ headers.get("x-api-key") ?? headers.get("authorization")?.replace(/^Bearer\s+/i, "");
28
+ return scrubSecrets(apiKey ? body.replaceAll(apiKey, "[REDACTED]") : body);
29
+ }
30
+
31
+ export interface LiveKeyResult {
32
+ keyType: string;
33
+ index: number;
34
+ suffix: string;
35
+ ok: boolean;
36
+ error?: string;
37
+ httpStatus?: number;
38
+ latency_ms: number;
39
+ }
40
+
41
+ export interface LiveValidationResult {
42
+ ok: boolean;
43
+ error?: string;
44
+ httpStatus?: number;
45
+ warning?: string;
46
+ keys?: LiveKeyResult[];
47
+ latency_ms: number;
48
+ /** No free live check exists for this credential (cloud routes): report presence only. */
49
+ skipped?: boolean;
50
+ }
51
+
52
+ export async function timedFetch(
53
+ url: string,
54
+ init: RequestInit,
55
+ options: ProbeOptions = {},
56
+ ): Promise<{ ok: boolean; status: number; bodyText: string; latency_ms: number }> {
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(() => controller.abort(), LIVE_TEST_TIMEOUT_MS);
59
+ const start = Date.now();
60
+ try {
61
+ if (options.publicEndpoint) assertUrlSafe(url, publicEndpointSsrfOptions());
62
+ const res = await fetch(url, {
63
+ ...init,
64
+ ...(options.publicEndpoint ? { redirect: "manual" } : {}),
65
+ signal: controller.signal,
66
+ });
67
+ let bodyText = "";
68
+ try {
69
+ bodyText = scrubProbeBody(await res.text(), init);
70
+ } catch {
71
+ // The HTTP status is sufficient when the response body cannot be read.
72
+ }
73
+ return {
74
+ ok: res.ok,
75
+ status: res.status,
76
+ bodyText,
77
+ latency_ms: Date.now() - start,
78
+ };
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ export async function checkAnthropicApiKey(
85
+ apiKey: string,
86
+ options: ProbeOptions = {},
87
+ ): Promise<LiveValidationResult> {
88
+ const r = await timedFetch(
89
+ "https://api.anthropic.com/v1/models",
90
+ {
91
+ method: "GET",
92
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
93
+ },
94
+ options,
95
+ );
96
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
97
+ return {
98
+ ok: false,
99
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
100
+ httpStatus: r.status,
101
+ latency_ms: r.latency_ms,
102
+ };
103
+ }
104
+
105
+ export async function checkOpenAiApiKey(
106
+ apiKey: string,
107
+ options: ProbeOptions = {},
108
+ ): Promise<LiveValidationResult> {
109
+ const r = await timedFetch(
110
+ "https://api.openai.com/v1/models",
111
+ {
112
+ method: "GET",
113
+ headers: { Authorization: `Bearer ${apiKey}` },
114
+ },
115
+ options,
116
+ );
117
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
118
+ return {
119
+ ok: false,
120
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
121
+ httpStatus: r.status,
122
+ latency_ms: r.latency_ms,
123
+ };
124
+ }
125
+
126
+ /** `GET /v1/models` is the call the Grok CLI makes first; it runs no inference. */
127
+ export async function checkXaiApiKey(
128
+ apiKey: string,
129
+ options: ProbeOptions = {},
130
+ ): Promise<LiveValidationResult> {
131
+ const r = await timedFetch(
132
+ "https://api.x.ai/v1/models",
133
+ {
134
+ method: "GET",
135
+ headers: { Authorization: `Bearer ${apiKey}` },
136
+ },
137
+ options,
138
+ );
139
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
140
+ return {
141
+ ok: false,
142
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
143
+ httpStatus: r.status,
144
+ latency_ms: r.latency_ms,
145
+ };
146
+ }
147
+
148
+ /** Fetch a key-authenticated GET and normalize it into a `LiveValidationResult`. */
149
+ async function checkBearerGet(
150
+ url: string,
151
+ init: RequestInit,
152
+ options: ProbeOptions = {},
153
+ ): Promise<LiveValidationResult> {
154
+ const r = await timedFetch(url, { method: "GET", ...init }, options);
155
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
156
+ return {
157
+ ok: false,
158
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
159
+ httpStatus: r.status,
160
+ latency_ms: r.latency_ms,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * OpenRouter's `/models` is public and answers 200 for any key, so the check
166
+ * reads `GET /key` (the key's own info, 401 for a bad key). A custom
167
+ * `OPENROUTER_BASE_URL` without `/key` (404/405) falls back to `/models`.
168
+ */
169
+ export async function checkOpenRouter(
170
+ apiKey: string,
171
+ options: ProbeOptions = {},
172
+ ): Promise<LiveValidationResult> {
173
+ const base = getOpenRouterBaseUrl(options.env);
174
+ const get = (url: string) =>
175
+ checkBearerGet(
176
+ url,
177
+ {
178
+ headers: {
179
+ ...getOpenRouterAttributionHeaders(url, options.env),
180
+ Authorization: `Bearer ${apiKey}`,
181
+ },
182
+ },
183
+ options,
184
+ );
185
+ const keyResult = await get(`${base}/key`);
186
+ if (keyResult.httpStatus === 404 || keyResult.httpStatus === 405) return get(`${base}/models`);
187
+ return keyResult;
188
+ }
189
+
190
+ /** `GET /models` on the official DeepSeek API: lists models, runs no inference. */
191
+ export function checkDeepSeekApiKey(
192
+ apiKey: string,
193
+ options: ProbeOptions = {},
194
+ ): Promise<LiveValidationResult> {
195
+ return checkBearerGet(
196
+ "https://api.deepseek.com/models",
197
+ {
198
+ headers: { Authorization: `Bearer ${apiKey}` },
199
+ },
200
+ options,
201
+ );
202
+ }
203
+
204
+ /**
205
+ * `GET <baseUrl>/models` with a Bearer key against an OpenAI-compatible
206
+ * registry provider (NaN, Helmcode). Lists models, runs no inference.
207
+ * `baseUrl` defaults to the registry entry; the probe passes the
208
+ * `<ID>_BASE_URL` override, and tests inject a fake server.
209
+ */
210
+ export async function checkOpenAICompatibleProvider(
211
+ provider: OpenAICompatibleProvider,
212
+ apiKey: string,
213
+ baseUrl: string = provider.baseUrl,
214
+ options: ProbeOptions = {},
215
+ ): Promise<LiveValidationResult> {
216
+ const r = await timedFetch(
217
+ `${baseUrl.replace(/\/+$/, "")}/models`,
218
+ {
219
+ method: "GET",
220
+ headers: { Authorization: `Bearer ${apiKey}` },
221
+ },
222
+ options,
223
+ );
224
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
225
+ return {
226
+ ok: false,
227
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
228
+ httpStatus: r.status,
229
+ latency_ms: r.latency_ms,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Run the route provider's own check. Requests stay on the route's origin and
235
+ * path prefix, go to public hosts only, and never follow a redirect.
236
+ */
237
+ async function checkRoute(
238
+ route: ModelRoute,
239
+ env: Record<string, string | undefined>,
240
+ ): Promise<LiveValidationResult> {
241
+ const startedAt = Date.now();
242
+ const provider = getRouteProvider(route.provider);
243
+ const baseUrl = route.baseUrl ?? provider?.defaultBaseUrl;
244
+ if (!provider?.validate || !baseUrl) return { ok: true, skipped: true, latency_ms: 0 };
245
+ const publicFetch = async (url: string | URL | Request, init?: RequestInit) => {
246
+ assertUrlSafe(String(url), publicEndpointSsrfOptions());
247
+ const res = await fetch(url, init);
248
+ if (res.ok || !res.body) return res;
249
+ return new Response(scrubProbeBody(await res.text(), init ?? {}), {
250
+ status: res.status,
251
+ statusText: res.statusText,
252
+ headers: res.headers,
253
+ });
254
+ };
255
+ const result = await provider.validate({
256
+ route,
257
+ env,
258
+ signal: AbortSignal.timeout(LIVE_TEST_TIMEOUT_MS),
259
+ fetch: createScopedFetch(baseUrl, publicFetch as typeof fetch),
260
+ });
261
+ const latency_ms = Date.now() - startedAt;
262
+ if (result.status === "verified") return { ok: true, latency_ms };
263
+ return {
264
+ ok: result.status === "configured",
265
+ ...(result.status === "configured" ? { skipped: true } : { httpStatus: result.httpStatus }),
266
+ error: scrubSecrets(result.reason),
267
+ latency_ms,
268
+ };
269
+ }
270
+
271
+ /** Check one newly submitted pool member without loading a worker harness. */
272
+ export async function checkProviderPoolKey(
273
+ keyType: string,
274
+ apiKey: string,
275
+ env: Record<string, string | undefined> = process.env,
276
+ ): Promise<LiveValidationResult> {
277
+ const startedAt = Date.now();
278
+ const options: ProbeOptions = { env, publicEndpoint: true };
279
+ try {
280
+ switch (keyType) {
281
+ case "ANTHROPIC_API_KEY": {
282
+ // Behind a gateway (`ANTHROPIC_BASE_URL`), Claude Code sends this key
283
+ // to the gateway, so check it there and not at api.anthropic.com.
284
+ const keyEnv = { ...env, ANTHROPIC_API_KEY: apiKey };
285
+ const route = deriveDefaultRoute("claude", keyEnv);
286
+ if (
287
+ route?.provider === "anthropic-gateway" &&
288
+ "secretKey" in route.auth &&
289
+ route.auth.secretKey === "ANTHROPIC_API_KEY"
290
+ ) {
291
+ return await checkRoute(route, keyEnv);
292
+ }
293
+ return await checkAnthropicApiKey(apiKey, options);
294
+ }
295
+ case "OPENAI_API_KEY":
296
+ return await checkOpenAiApiKey(apiKey, options);
297
+ case "OPENROUTER_API_KEY":
298
+ return await checkOpenRouter(apiKey, options);
299
+ case "DEEPSEEK_API_KEY":
300
+ return await checkDeepSeekApiKey(apiKey, options);
301
+ case "XAI_API_KEY":
302
+ return await checkXaiApiKey(apiKey, options);
303
+ case "CLAUDE_CODE_OAUTH_TOKEN": {
304
+ const tokenEnv = { CLAUDE_CODE_OAUTH_TOKEN: apiKey };
305
+ return await checkRoute(deriveDefaultRoute("claude", tokenEnv)!, tokenEnv);
306
+ }
307
+ default: {
308
+ const provider = providerForKeyVar(keyType);
309
+ if (provider) {
310
+ return await checkOpenAICompatibleProvider(
311
+ provider,
312
+ apiKey,
313
+ providerBaseUrl(provider, env),
314
+ options,
315
+ );
316
+ }
317
+ return { ok: true, skipped: true, latency_ms: 0 };
318
+ }
319
+ }
320
+ } catch (err) {
321
+ const raw = err instanceof Error ? err.message : String(err);
322
+ return {
323
+ ok: false,
324
+ error: scrubSecrets(raw.split(apiKey).join("[REDACTED]")) || "Unknown error",
325
+ latency_ms: Date.now() - startedAt,
326
+ };
327
+ }
328
+ }
@@ -20,6 +20,7 @@
20
20
  * auto-detected key always goes to its provider's own endpoint. Deepgram and AssemblyAI use other request shapes and are not
21
21
  * supported. Read at call time, so a config reload applies without a restart.
22
22
  */
23
+ import { selectCredential } from "./credentials";
23
24
  import { createValueScrubber, scrubSecrets } from "./secret-scrubber";
24
25
 
25
26
  const TRANSCRIPTION_TIMEOUT_MS = 60_000;
@@ -119,7 +120,9 @@ export function resolveTranscriptionConfig(): TranscriptionConfig | null {
119
120
  : AUTO_DETECT_ORDER;
120
121
  for (const name of candidates) {
121
122
  const { keyEnv } = PROVIDERS[name];
122
- const apiKey = envValue(keyEnv);
123
+ const value = envValue(keyEnv);
124
+ // OPENAI_API_KEY may be a comma pool: send one member.
125
+ const apiKey = value ? selectCredential(value, undefined, keyEnv).selected : undefined;
123
126
  if (apiKey) return { provider: name, apiKey, keyEnv, source: "auto" };
124
127
  }
125
128
  return null;
@@ -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
- }
@@ -133,7 +133,14 @@ export class SubWorkflowExecutor extends BaseExecutor<
133
133
  throw err;
134
134
  }
135
135
  const child = await db.getWorkflowRun(childRunId);
136
- if (!child) return { status: "failed", error: `Child workflow run ${childRunId} not found` };
136
+ // The start committed the row, so a missing one was deleted with its workflow.
137
+ if (!child) {
138
+ return {
139
+ status: "failed",
140
+ error: `Child workflow run ${childRunId} not found`,
141
+ retryable: false,
142
+ };
143
+ }
137
144
  return this.settle(child, config.workflowId);
138
145
  }
139
146
 
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "5a577022800d9b43ccec5376bfe16c7b24eecd91",
2
+ "commit": "c81f2bb62f49b45603e9e702558e1ca22e98fcdd",
3
3
  "excludedSkills": [
4
4
  "feedback"
5
5
  ],
@@ -44,7 +44,7 @@
44
44
  "templates/skills/questioning/content.md": "66f826a191e96d0041adca00a7c197a458de8e6fe164c1599b8708098d4109ab",
45
45
  "templates/skills/questioning/files/template.md": "a68d72801a0b10380bf773b07e9d2f8d4c5b4b1c609ce954289e38ffc78cc223",
46
46
  "templates/skills/researching/config.json": "921c062cab599097cc7677d2dad4797ec4d5844d2ee73607fbd417ec3fa487d4",
47
- "templates/skills/researching/content.md": "92951142327415696726a2577defdb2379cc611cdafa9cdac92431a5e60f618f",
47
+ "templates/skills/researching/content.md": "87ced42d15f1fd63d1b4e8d5ed8161537202f5b05a85df38dea273e9e3d6ad08",
48
48
  "templates/skills/researching/files/template.md": "411d34b98c610ef715edf0f24d09f828ba2d0f65f688e7879ef33a9453007949",
49
49
  "templates/skills/reviewing/config.json": "09c40bb098e9738c60a848995fa9fe7568d158631a089c76ba02c0dd955445c7",
50
50
  "templates/skills/reviewing/content.md": "1bbc285c9ad8cc3312f1ee2c558b04ffee914ffb6f5c63fb3741fcbb3e300fa5",