@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
@@ -98,6 +98,45 @@ describe("script SDK allowlist", () => {
98
98
  }
99
99
  });
100
100
 
101
+ test("workflow_getRun forwards fields to the run route and rejects an unknown value", async () => {
102
+ const requests: URL[] = [];
103
+ const httpServer = Bun.serve({
104
+ port: 0,
105
+ fetch(req) {
106
+ requests.push(new URL(req.url));
107
+ return Response.json({ run: { id: "run-1" }, steps: [] });
108
+ },
109
+ });
110
+ const config = new SwarmConfig({
111
+ system: {
112
+ apiKey: { value: "sdk-test-key", isSecret: true },
113
+ agentId: { value: "sdk-test-agent", isSecret: false },
114
+ mcpBaseUrl: { value: `http://127.0.0.1:${httpServer.port}`, isSecret: false },
115
+ },
116
+ user: {},
117
+ });
118
+ const sdk = createSwarmSdk(config);
119
+
120
+ try {
121
+ await sdk.workflow_getRun({ id: "run-1", fields: "slim" });
122
+ await sdk.workflow_getRun({ id: "run-1" });
123
+ expect(requests.map((url) => url.pathname)).toEqual([
124
+ "/api/workflow-runs/run-1",
125
+ "/api/workflow-runs/run-1",
126
+ ]);
127
+ expect(requests[0]?.searchParams.get("fields")).toBe("slim");
128
+ // Omitted `fields` sends no query, so the route keeps its full default.
129
+ expect(requests[1]?.search).toBe("");
130
+
131
+ await expect(sdk.workflow_getRun({ id: "run-1", fields: "tiny" })).rejects.toThrow(
132
+ '`fields` must be "full" or "slim"',
133
+ );
134
+ expect(requests).toHaveLength(2);
135
+ } finally {
136
+ httpServer.stop(true);
137
+ }
138
+ });
139
+
101
140
  test("approval_cancel posts to the cancel route with the reason", async () => {
102
141
  let request: { method: string; path: string; body: unknown } | undefined;
103
142
  const httpServer = Bun.serve({
@@ -252,8 +291,9 @@ describe("script SDK allowlist", () => {
252
291
 
253
292
  test("bundled swarm-sdk.d.ts uses triggerData (not input) for workflow_trigger", async () => {
254
293
  const types = await Bun.file("src/scripts-runtime/types/swarm-sdk.d.ts").text();
255
- expect(types).toContain("workflow_trigger(args: { id: string; triggerData?");
256
- expect(types).not.toContain("workflow_trigger(args: { id: string; input?");
294
+ expect(types).toMatch(/workflow_trigger\(args: \{\s*id: string;\s*triggerData\?/);
295
+ expect(types).toMatch(/workflow_trigger\(args: \{[^}]*\bwait\?: boolean;/);
296
+ expect(types).not.toMatch(/workflow_trigger\(args: \{\s*id: string;\s*input\?/);
257
297
  });
258
298
  });
259
299
 
@@ -503,17 +503,17 @@ describe("Self-Improvement Mechanisms", () => {
503
503
  const MEMORY_SKILL_POINTER =
504
504
  "You MUST use the `memory` skill before you store, edit, or delete a memory.";
505
505
 
506
- test("a worker prompt names memory-store and the memory skill", async () => {
506
+ test("a worker prompt points learnings at store-progress and names the memory skill", async () => {
507
507
  const prompt = await getBasePrompt({ role: "worker", agentId: workerId });
508
508
 
509
- expect(prompt).toContain("`memory-store`");
509
+ expect(prompt).toContain("as `learning` on a `store-progress` call");
510
510
  expect(prompt).toContain(MEMORY_SKILL_POINTER);
511
511
  });
512
512
 
513
- test("a lead prompt names memory-store and the memory skill", async () => {
513
+ test("a lead prompt points learnings at store-progress and names the memory skill", async () => {
514
514
  const prompt = await getBasePrompt({ role: "lead", agentId: leadId });
515
515
 
516
- expect(prompt).toContain("`memory-store`");
516
+ expect(prompt).toContain("as `learning` on a `store-progress` call");
517
517
  expect(prompt).toContain(MEMORY_SKILL_POINTER);
518
518
  });
519
519
 
@@ -0,0 +1,429 @@
1
+ import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
2
+ import { defaultAssetKey } from "../assets/key";
3
+ import { sessionSourceKey } from "../assets/source-key";
4
+ import * as audit from "../be/asset-key-audit";
5
+ import {
6
+ closeDb,
7
+ createTaskExtended,
8
+ createUser,
9
+ getDbClient,
10
+ getTaskById,
11
+ initDb,
12
+ insertTaskAttachment,
13
+ listRecentSessions,
14
+ upsertInboxState,
15
+ } from "../be/db";
16
+ import * as sessionMoves from "../be/db/session-moves";
17
+ import type { DbParam } from "../be/db-client";
18
+ import { runSessionSourceBackfill } from "../be/session-source-backfill";
19
+ import { chatSdkContextKey } from "../tasks/context-key";
20
+
21
+ const DONE_KEY = "session-source-keys-v1";
22
+ const CURSOR_KEY = `${DONE_KEY}:cursor`;
23
+ const BEFORE = "2026-10-21T10:30:00.000Z";
24
+ const DONE_AT = "2026-10-21T11:00:00.000Z";
25
+ const slack = { slackChannelId: "C0AR967K0KZ", slackThreadTs: "1729000000.123456" };
26
+ let owner: string;
27
+
28
+ beforeEach(async () => {
29
+ initDb(":memory:");
30
+ owner = (await createUser({ name: "Backfill owner" })).id;
31
+ });
32
+ afterEach(() => closeDb());
33
+
34
+ // SQL preserves pre-source-key rows, including uppercase legacy task ids.
35
+ async function seedTask(
36
+ opts: {
37
+ id?: string;
38
+ key?: string;
39
+ source?: string;
40
+ parentTaskId?: string;
41
+ requestedByUserId?: string | null;
42
+ slackChannelId?: string;
43
+ slackThreadTs?: string;
44
+ contextKey?: string;
45
+ } = {},
46
+ ) {
47
+ const id = opts.id ?? crypto.randomUUID();
48
+ await getDbClient().run(
49
+ `INSERT INTO agent_tasks
50
+ (id, task, status, source, parentTaskId, requestedByUserId, "key",
51
+ slackChannelId, slackThreadTs, contextKey, createdAt, lastUpdatedAt, updated_by)
52
+ VALUES (?, 'Old session', 'completed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
53
+ [
54
+ id,
55
+ opts.source ?? "slack",
56
+ opts.parentTaskId ?? null,
57
+ opts.requestedByUserId === undefined ? owner : opts.requestedByUserId,
58
+ opts.key ?? defaultAssetKey("task", id),
59
+ opts.slackChannelId ?? null,
60
+ opts.slackThreadTs ?? null,
61
+ opts.contextKey ?? null,
62
+ BEFORE,
63
+ BEFORE,
64
+ owner,
65
+ ],
66
+ );
67
+ return (await getTaskById(id))!;
68
+ }
69
+
70
+ async function seedState(key: string) {
71
+ return getDbClient().get<{ seededHash: string }>(
72
+ "SELECT seededHash FROM seed_state WHERE kind = 'maintenance' AND key = ?",
73
+ [key],
74
+ );
75
+ }
76
+
77
+ test("moves both old defaults, whole chains and mapped files with history, without changing read or Done state", async () => {
78
+ const roots = [
79
+ await seedTask({ ...slack, key: "shared/" }),
80
+ await seedTask({
81
+ ...slack,
82
+ slackThreadTs: "1729000000.654321",
83
+ id: crypto.randomUUID().toUpperCase(),
84
+ }),
85
+ ];
86
+ const expectedHistory: unknown[] = [];
87
+ const tasks = roots.map((task) => ({ task, key: sessionSourceKey(task) }));
88
+ const mappings: { id: string; key: string | null }[] = [];
89
+ for (const [index, root] of roots.entries()) {
90
+ const child = await seedTask({ parentTaskId: root.id, key: root.key, source: "mcp" });
91
+ const grandchild = await seedTask({
92
+ parentTaskId: child.id,
93
+ key: "shared/other-child-key/",
94
+ source: "mcp",
95
+ });
96
+ tasks.push(...[child, grandchild].map((task) => ({ task, key: sessionSourceKey(root) })));
97
+ const attachment = await insertTaskAttachment({
98
+ taskId: grandchild.id,
99
+ kind: "agent-fs",
100
+ name: "notes",
101
+ path: `backfill/${grandchild.id}`,
102
+ providerId: "agent-fs",
103
+ providerKey: `backfill/${grandchild.id}`,
104
+ });
105
+ const mapping = await getDbClient().get<{ id: string; key: string }>(
106
+ "SELECT id, key FROM asset_key_mappings WHERE source_entity_id = ?",
107
+ [attachment.id],
108
+ );
109
+ expect(mapping).not.toBeNull();
110
+ mappings.push({ id: mapping!.id, key: sessionSourceKey(root) });
111
+ await getDbClient().run("UPDATE asset_key_mappings SET updated_by = ? WHERE id = ?", [
112
+ owner,
113
+ mapping!.id,
114
+ ]);
115
+ for (const task of [root, child, grandchild]) {
116
+ expectedHistory.push({
117
+ entity_type: "task",
118
+ entity_id: task.id,
119
+ previous_key: task.key,
120
+ new_key: sessionSourceKey(root),
121
+ changed_by: null,
122
+ });
123
+ }
124
+ expectedHistory.push({
125
+ entity_type: "file",
126
+ entity_id: mapping!.id,
127
+ previous_key: mapping!.key,
128
+ new_key: sessionSourceKey(root),
129
+ changed_by: null,
130
+ });
131
+ await upsertInboxState({
132
+ userId: owner,
133
+ itemType: "to_read",
134
+ itemId: root.id,
135
+ status: index === 0 ? "done" : "open",
136
+ doneAt: index === 0 ? DONE_AT : undefined,
137
+ readAt: BEFORE,
138
+ lastReadAt: BEFORE,
139
+ });
140
+ }
141
+ const inboxBefore = await getDbClient().query("SELECT * FROM inbox_item_state ORDER BY itemId");
142
+ const sessionsBefore = await listRecentSessions({ viewer: { userId: owner, scope: "all" } });
143
+ expect(sessionsBefore.find((s) => s.root.id === roots[0]!.id)?.readState).toBe("done");
144
+ const historyBefore = await getDbClient().query(
145
+ "SELECT entity_type, entity_id, previous_key, new_key, changed_by FROM asset_key_history",
146
+ );
147
+ const auditSpy = spyOn(audit, "auditAssetKeys");
148
+ try {
149
+ expect(await runSessionSourceBackfill({ batchSize: 1 })).toEqual({
150
+ scanned: 2,
151
+ moved: 2,
152
+ skipped: 0,
153
+ failed: 0,
154
+ });
155
+ expect(auditSpy).toHaveBeenCalledTimes(1);
156
+ } finally {
157
+ auditSpy.mockRestore();
158
+ }
159
+ for (const { task, key } of tasks) {
160
+ const after = await getTaskById(task.id);
161
+ expect(after?.key).toBe(key);
162
+ expect(after?.lastUpdatedAt).toBe(task.lastUpdatedAt);
163
+ expect(
164
+ await getDbClient().get("SELECT updated_by FROM agent_tasks WHERE id = ?", [task.id]),
165
+ ).toEqual({ updated_by: owner });
166
+ }
167
+ for (const { id, key } of mappings) {
168
+ expect(
169
+ await getDbClient().get("SELECT key, updated_by FROM asset_key_mappings WHERE id = ?", [id]),
170
+ ).toEqual({ key, updated_by: owner });
171
+ }
172
+ const history = await getDbClient().query(
173
+ "SELECT entity_type, entity_id, previous_key, new_key, changed_by FROM asset_key_history",
174
+ );
175
+ expect(history).toHaveLength(historyBefore.length + expectedHistory.length);
176
+ expect(history).toEqual(expect.arrayContaining([...historyBefore, ...expectedHistory]));
177
+ expect(await getDbClient().query("SELECT * FROM inbox_item_state ORDER BY itemId")).toEqual(
178
+ inboxBefore,
179
+ );
180
+ const sessionsAfter = await listRecentSessions({ viewer: { userId: owner, scope: "all" } });
181
+ for (const before of sessionsBefore) {
182
+ expect(sessionsAfter.find((s) => s.root.id === before.root.id)?.readState).toBe(
183
+ before.readState,
184
+ );
185
+ }
186
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
187
+ expect(await seedState(CURSOR_KEY)).toBeNull();
188
+ });
189
+
190
+ test("preserves user keys and unsupported roots, and moves mcp and ownerless Slack roots", async () => {
191
+ const custom = await seedTask({ ...slack, key: "shared/research/" });
192
+ const child = await seedTask({ ...slack, parentTaskId: custom.id, key: "shared/" });
193
+ const foreignDefault = await seedTask({ ...slack, key: defaultAssetKey("task", custom.id) });
194
+ const teams = await seedTask({ source: "chat", contextKey: chatSdkContextKey("teams:c:t") });
195
+ const ordinary = await seedTask({ source: "ui" });
196
+ const missingThread = await seedTask({ slackChannelId: slack.slackChannelId });
197
+ const mcp = await seedTask({ ...slack, source: "mcp" });
198
+ const ownerless = await seedTask({ ...slack, requestedByUserId: null });
199
+ expect(await runSessionSourceBackfill()).toEqual({ scanned: 3, moved: 2, skipped: 1, failed: 0 });
200
+ for (const task of [custom, child, foreignDefault, teams, ordinary, missingThread]) {
201
+ expect(await getTaskById(task.id)).toEqual(task);
202
+ }
203
+ for (const task of [mcp, ownerless]) {
204
+ expect((await getTaskById(task.id))?.key).toBe(sessionSourceKey(task));
205
+ }
206
+ expect((await getTaskById(ownerless.id))?.requestedByUserId).toBeUndefined();
207
+ });
208
+
209
+ test("moves supported chat roots", async () => {
210
+ const threads = [
211
+ ["telegram:-1001234567890:42", "shared/telegram/-1001234567890/thread:42/"],
212
+ ["telegram:555", "shared/telegram/555/task:00000000-0000-4000-8000-000000000001/"],
213
+ ["discord:guild:channel:thread", "shared/discord/channel/thread:thread/"],
214
+ ["discord:@me:dm", "shared/discord/dm/task:00000000-0000-4000-8000-000000000003/"],
215
+ ["slack:C123:1.2", "shared/slack/c123/thread:1.2/"],
216
+ ["slack:D123:", "shared/slack/d123/task:00000000-0000-4000-8000-000000000005/"],
217
+ ];
218
+ const roots = [];
219
+ for (const [index, [thread, key]] of threads.entries()) {
220
+ const root = await seedTask({
221
+ id: `00000000-0000-4000-8000-00000000000${index}`,
222
+ source: "chat",
223
+ contextKey: chatSdkContextKey(thread!),
224
+ });
225
+ roots.push({ root, key });
226
+ }
227
+ const invalid = await seedTask({ ...slack, slackChannelId: "x".repeat(255) });
228
+ expect(await runSessionSourceBackfill()).toEqual({ scanned: 7, moved: 6, skipped: 1, failed: 0 });
229
+ for (const { root, key } of roots) {
230
+ expect((await getTaskById(root.id))?.key).toBe(key);
231
+ }
232
+ expect(await getTaskById(invalid.id)).toEqual(invalid);
233
+ });
234
+
235
+ test("a completed run does not scan or audit again, even if another old root appears", async () => {
236
+ await seedTask(slack);
237
+ await runSessionSourceBackfill();
238
+ const before = await getDbClient().query("SELECT * FROM asset_key_history ORDER BY id");
239
+ const later = await seedTask(slack);
240
+ const auditSpy = spyOn(audit, "auditAssetKeys");
241
+ try {
242
+ expect(await runSessionSourceBackfill()).toBeNull();
243
+ expect(auditSpy).not.toHaveBeenCalled();
244
+ } finally {
245
+ auditSpy.mockRestore();
246
+ }
247
+ expect(await getTaskById(later.id)).toEqual(later);
248
+ expect(await getDbClient().query("SELECT * FROM asset_key_history ORDER BY id")).toEqual(before);
249
+ });
250
+
251
+ test("isolates a failed root and retries it from the start on the next run", async () => {
252
+ const roots = [];
253
+ for (let index = 0; index < 3; index++) {
254
+ roots.push(await seedTask({ ...slack, id: `00000000-0000-4000-8000-00000000000${index}` }));
255
+ }
256
+ const move = sessionMoves.moveTaskChainKeyInTx;
257
+ const moveSpy = spyOn(sessionMoves, "moveTaskChainKeyInTx").mockImplementationOnce(
258
+ async (...args) => {
259
+ await move(...args);
260
+ throw new Error("Root move failed");
261
+ },
262
+ );
263
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
264
+ try {
265
+ expect(await runSessionSourceBackfill({ batchSize: 1 })).toEqual({
266
+ scanned: 3,
267
+ moved: 2,
268
+ skipped: 0,
269
+ failed: 1,
270
+ });
271
+ expect(moveSpy).toHaveBeenCalledTimes(3);
272
+ expect(moveSpy.mock.calls[0]?.[0]).toBe(roots[0]!.id);
273
+ expect(warn).toHaveBeenCalledTimes(2);
274
+ expect(warn.mock.calls[0]?.[0]).toContain(`root=${roots[0]!.id} failed: Root move failed`);
275
+ expect(await getTaskById(roots[0]!.id)).toEqual(roots[0]!);
276
+ for (const root of roots.slice(1)) {
277
+ expect((await getTaskById(root.id))?.key).toBe(sessionSourceKey(root));
278
+ }
279
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(2);
280
+ expect(await seedState(DONE_KEY)).toBeNull();
281
+ expect(await seedState(CURSOR_KEY)).toBeNull();
282
+ } finally {
283
+ moveSpy.mockRestore();
284
+ warn.mockRestore();
285
+ }
286
+ expect(await runSessionSourceBackfill()).toEqual({ scanned: 1, moved: 1, skipped: 0, failed: 0 });
287
+ expect((await getTaskById(roots[0]!.id))?.key).toBe(sessionSourceKey(roots[0]!));
288
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(3);
289
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
290
+ expect(await seedState(CURSOR_KEY)).toBeNull();
291
+ });
292
+
293
+ test("preserves a user move and includes a child created after the batch read", async () => {
294
+ const movedByUser = await seedTask(slack);
295
+ const root = await seedTask(slack);
296
+ const client = getDbClient();
297
+ const query = client.query.bind(client);
298
+ let childId: string | undefined;
299
+ const querySpy = spyOn(client, "query").mockImplementation(
300
+ async <T>(sql: string, params?: DbParam[]): Promise<T[]> => {
301
+ const rows = await query<T>(sql, params);
302
+ if (sql.startsWith("SELECT id, slackChannelId") && rows.length > 0) {
303
+ await client.run('UPDATE agent_tasks SET "key" = ? WHERE id = ?', [
304
+ "shared/research/",
305
+ movedByUser.id,
306
+ ]);
307
+ childId = (await seedTask({ parentTaskId: root.id, key: root.key, source: "mcp" })).id;
308
+ }
309
+ return rows;
310
+ },
311
+ );
312
+ try {
313
+ expect(await runSessionSourceBackfill()).toEqual({
314
+ scanned: 2,
315
+ moved: 1,
316
+ skipped: 1,
317
+ failed: 0,
318
+ });
319
+ expect(childId).toBeDefined();
320
+ } finally {
321
+ querySpy.mockRestore();
322
+ }
323
+ expect((await getTaskById(movedByUser.id))?.key).toBe("shared/research/");
324
+ expect((await getTaskById(root.id))?.key).toBe(sessionSourceKey(root));
325
+ expect((await getTaskById(childId!))?.key).toBe(sessionSourceKey(root));
326
+ });
327
+
328
+ test("a child whose parent moves after the parent read still inherits the moved key", async () => {
329
+ const root = await seedTask(slack);
330
+ const client = getDbClient();
331
+ const transaction = client.transaction.bind(client);
332
+ let backfilled = false;
333
+ // The first transaction is the child's INSERT: the backfill commits first.
334
+ const txSpy = spyOn(client, "transaction").mockImplementationOnce(async (fn, opts) => {
335
+ txSpy.mockRestore();
336
+ backfilled = true;
337
+ expect(await runSessionSourceBackfill()).toMatchObject({ moved: 1, failed: 0 });
338
+ return transaction(fn, opts);
339
+ });
340
+ let child: Awaited<ReturnType<typeof createTaskExtended>>;
341
+ try {
342
+ child = await createTaskExtended("Child", { parentTaskId: root.id });
343
+ } finally {
344
+ txSpy.mockRestore();
345
+ }
346
+ expect(backfilled).toBe(true);
347
+ expect((await getTaskById(root.id))?.key).toBe(sessionSourceKey(root));
348
+ expect(child.key).toBe(sessionSourceKey(root));
349
+ });
350
+
351
+ test("resumes after a committed cursor, including skipped roots, when afterBatch throws once", async () => {
352
+ const roots = [
353
+ await seedTask({
354
+ id: "00000000-0000-4000-8000-000000000001",
355
+ source: "chat",
356
+ contextKey: chatSdkContextKey("teams:c:t"),
357
+ }),
358
+ await seedTask({ id: "00000000-0000-4000-8000-000000000002", ...slack }),
359
+ await seedTask({ id: "00000000-0000-4000-8000-000000000003", ...slack }),
360
+ await seedTask({ id: "00000000-0000-4000-8000-000000000004", ...slack }),
361
+ await seedTask({ id: "00000000-0000-4000-8000-000000000005", ...slack }),
362
+ ];
363
+ let firstBatch: string[] = [];
364
+ await expect(
365
+ runSessionSourceBackfill({
366
+ batchSize: 2,
367
+ afterBatch: ({ ids }) => {
368
+ firstBatch = ids;
369
+ throw new Error("Interrupted after batch 1");
370
+ },
371
+ }),
372
+ ).rejects.toThrow("Interrupted after batch 1");
373
+ expect(firstBatch).toEqual(roots.slice(0, 2).map((r) => r.id));
374
+ expect(await seedState(CURSOR_KEY)).toEqual({ seededHash: roots[1]!.id });
375
+ expect(await seedState(DONE_KEY)).toBeNull();
376
+ expect((await getTaskById(roots[1]!.id))?.key).toBe(sessionSourceKey(roots[1]!));
377
+ expect((await getTaskById(roots[2]!.id))?.key).toBe(roots[2]!.key);
378
+ const resumed: string[] = [];
379
+ expect(
380
+ await runSessionSourceBackfill({
381
+ batchSize: 2,
382
+ afterBatch: ({ ids }) => {
383
+ resumed.push(...ids);
384
+ },
385
+ }),
386
+ ).toEqual({ scanned: 3, moved: 3, skipped: 0, failed: 0 });
387
+ expect(resumed).toEqual(roots.slice(2).map((r) => r.id));
388
+ expect(await getTaskById(roots[0]!.id)).toEqual(roots[0]!);
389
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(4);
390
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
391
+ expect(await seedState(CURSOR_KEY)).toBeNull();
392
+ });
393
+
394
+ test("a blocked audit warns without a marker or cursor, then retries successfully", async () => {
395
+ const root = await seedTask(slack);
396
+ const originalAudit = audit.auditAssetKeys;
397
+ const auditSpy = spyOn(audit, "auditAssetKeys").mockImplementation((db) => ({
398
+ ...originalAudit(db),
399
+ ok: false,
400
+ warningCount: 1,
401
+ }));
402
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
403
+ try {
404
+ expect(await runSessionSourceBackfill()).toBeNull();
405
+ expect(auditSpy).toHaveBeenCalledTimes(1);
406
+ expect(warn).toHaveBeenCalledTimes(1);
407
+ expect(await seedState(DONE_KEY)).toBeNull();
408
+ expect(await seedState(CURSOR_KEY)).toBeNull();
409
+ expect(await getTaskById(root.id)).toEqual(root);
410
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(0);
411
+ } finally {
412
+ auditSpy.mockRestore();
413
+ warn.mockRestore();
414
+ }
415
+ expect((await runSessionSourceBackfill())?.moved).toBe(1);
416
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
417
+ });
418
+
419
+ test("unexpected audit errors propagate without a marker", async () => {
420
+ const auditSpy = spyOn(audit, "auditAssetKeys").mockImplementation(() => {
421
+ throw new TypeError("Unexpected audit failure");
422
+ });
423
+ try {
424
+ await expect(runSessionSourceBackfill()).rejects.toThrow("Unexpected audit failure");
425
+ expect(await seedState(DONE_KEY)).toBeNull();
426
+ } finally {
427
+ auditSpy.mockRestore();
428
+ }
429
+ });