@desplega.ai/agent-swarm 2.5.3 → 2.6.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 (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  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/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  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/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  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/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  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/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -1,9 +1,16 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
2
  import { createServer, type Server } from "node:http";
3
3
  import { keyPrefixRange, sessionGroupKey } from "../assets/key";
4
+ import {
5
+ isSourceThreadKey,
6
+ NESTED_THREAD_MIN_ROOTS,
7
+ sessionThreadGroupKey,
8
+ } from "../assets/source-key";
4
9
  import {
5
10
  closeDb,
6
11
  countSessions,
12
+ createAgent,
13
+ createSessionCost,
7
14
  createSteeringMessage,
8
15
  createTaskExtended,
9
16
  createUser,
@@ -12,10 +19,12 @@ import {
12
19
  listRecentSessions,
13
20
  upsertInboxState,
14
21
  } from "../be/db";
15
- import { getSessionTree, type SessionFilters } from "../be/db/session-tree";
22
+ import { rememberSessionLabel } from "../be/db/session-labels";
23
+ import { getSessionTree, type SessionFilters, threadRootsSql } from "../be/db/session-tree";
16
24
  import { setSessionShares } from "../be/session-shares";
17
25
  import { handleSessions } from "../http/sessions";
18
26
  import { getPathSegments, parseQueryParams } from "../http/utils";
27
+ import { chatSdkContextKey } from "../tasks/context-key";
19
28
  import { beginRequestAuthScope, setRequestAuth } from "../utils/request-auth-context";
20
29
  import { listenOnFreePort } from "./test-net";
21
30
 
@@ -63,6 +72,7 @@ beforeEach(async () => {
63
72
  await getDbClient().run("DELETE FROM inbox_item_state");
64
73
  await getDbClient().run("DELETE FROM session_shares");
65
74
  await getDbClient().run("DELETE FROM agent_tasks");
75
+ await getDbClient().run("DELETE FROM kv_entries WHERE namespace = 'session-labels'");
66
76
  });
67
77
  afterAll(async () => {
68
78
  server.closeAllConnections();
@@ -168,7 +178,9 @@ describe("session groups and prefix bounds", () => {
168
178
  const query = spyOn(client, "query");
169
179
  let call: (typeof query.mock.calls)[number];
170
180
  try {
171
- await listRecentSessions(filters({ groupKey: "shared/a/" }));
181
+ await listRecentSessions(
182
+ filters({ groupKey: "shared/a/", showSource: ["slack"], hideSource: ["ui"] }),
183
+ );
172
184
  call = query.mock.calls[0]!;
173
185
  } finally {
174
186
  query.mockRestore();
@@ -189,6 +201,232 @@ describe("session groups and prefix bounds", () => {
189
201
  });
190
202
  });
191
203
 
204
+ describe("nested source thread groups", () => {
205
+ const channel = "shared/slack/c1/";
206
+ const thread = `${channel}thread:1.2/`;
207
+ const detailGroupKey = async (id: string) => {
208
+ const res = await fetch(`${baseUrl}/api/sessions/${id}`, {
209
+ headers: { "x-swarm-user-id": owner.id },
210
+ });
211
+ expect(res.status).toBe(200);
212
+ return ((await res.json()) as { groupKey: string }).groupKey;
213
+ };
214
+
215
+ test("two owned roots nest, lists agree, and a move collapses the group", async () => {
216
+ const first = await task(thread, { source: "slack" });
217
+ const second = await task(thread, { source: "mcp" }, NOW);
218
+ await getDbClient().run("UPDATE agent_tasks SET createdAt = ?, title = ? WHERE id = ?", [
219
+ OLD,
220
+ "First conversation",
221
+ first.id,
222
+ ]);
223
+ await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
224
+ second.id,
225
+ ]);
226
+ await pending(second.id);
227
+ const single = await task(`${channel}thread:3/`);
228
+ const tree = await getSessionTree(actor(), NOW);
229
+ expect(tree.groups).toEqual([
230
+ { key: channel, lastActivityAt: RECENT, total: 1, unread: 1, running: 0, needsYou: 0 },
231
+ { key: thread, lastActivityAt: NOW, total: 2, unread: 2, running: 1, needsYou: 1 },
232
+ ]);
233
+ expect(tree.threadLabels).toEqual({ [thread]: "First conversation" });
234
+ expect(tree.sourceGroups).toEqual([channel, thread]);
235
+ expect(await ids({ groupKey: channel })).toEqual([single.id]);
236
+ expect(await ids({ groupKey: thread })).toEqual([first.id, second.id].sort());
237
+ expect(await countSessions(filters({ groupKey: channel }))).toBe(1);
238
+ expect(await countSessions(filters({ groupKey: thread }))).toBe(2);
239
+ const rows = await listRecentSessions(filters());
240
+ expect(rows.find((r) => r.root.id === first.id)?.groupKey).toBe(thread);
241
+ expect(rows.find((r) => r.root.id === single.id)?.groupKey).toBe(channel);
242
+ // The session pane reveals the detail's group: the same rule as the list rows.
243
+ expect(await detailGroupKey(first.id)).toBe(thread);
244
+ expect(await detailGroupKey(single.id)).toBe(channel);
245
+
246
+ const moved = await fetch(`${baseUrl}/api/session-moves`, {
247
+ method: "POST",
248
+ headers: { "x-swarm-user-id": owner.id, "content-type": "application/json" },
249
+ body: JSON.stringify({ rootTaskIds: [second.id], groupKey: "shared/research/" }),
250
+ });
251
+ expect(moved.status).toBe(200);
252
+ expect(
253
+ (await getSessionTree(actor(), NOW)).groups.find((g) => g.key === thread),
254
+ ).toBeUndefined();
255
+ expect(await ids({ groupKey: channel })).toEqual([first.id, single.id].sort());
256
+ expect(
257
+ (await listRecentSessions(filters())).find((r) => r.root.id === first.id)?.groupKey,
258
+ ).toBe(channel);
259
+ expect(await detailGroupKey(first.id)).toBe(channel);
260
+ });
261
+
262
+ test("one owned root stays a channel row despite children and ownerless roots", async () => {
263
+ const root = await task(thread);
264
+ await task(thread, { parentTaskId: root.id });
265
+ await task(thread, { requestedByUserId: undefined });
266
+ const tree = await getSessionTree(actor(), NOW);
267
+ expect(tree.groups.map((g) => [g.key, g.total])).toEqual([[channel, 1]]);
268
+ expect(tree.threadLabels).toEqual({});
269
+ expect((await listRecentSessions(filters({ tree: true })))[0].groupKey).toBe(channel);
270
+ });
271
+
272
+ test("nesting counts private roots but labels use only roots visible to each user", async () => {
273
+ const visible = await task(thread, { source: "slack" });
274
+ const hidden = await task(thread, { requestedByUserId: other.id });
275
+ await getDbClient().run("UPDATE agent_tasks SET title = ? WHERE id = ?", [
276
+ "Visible title",
277
+ visible.id,
278
+ ]);
279
+ await getDbClient().run("UPDATE agent_tasks SET createdAt = ?, title = ? WHERE id = ?", [
280
+ OLD,
281
+ "Hidden oldest",
282
+ hidden.id,
283
+ ]);
284
+ const tree = await getSessionTree(actor(), NOW, { mine: true, showSource: ["slack"] });
285
+ expect(tree.groups.map((g) => [g.key, g.total])).toEqual([[thread, 1]]);
286
+ expect(tree.threadLabels[thread]).toBe("Visible title");
287
+ expect(await ids({ groupKey: channel })).toEqual([]);
288
+ expect(await ids({ groupKey: thread })).toEqual([visible.id]);
289
+ const filtered = await getSessionTree(actor(), NOW, { showSource: ["ui"] });
290
+ expect(filtered.groups).toEqual([]);
291
+ expect(filtered.sourceGroups).toEqual([channel, thread]);
292
+ expect(filtered.threadLabels[thread]).toBe("Visible title");
293
+ const otherTree = await getSessionTree({ kind: "user", userId: other.id }, NOW);
294
+ expect(otherTree.groups.map((g) => [g.key, g.total])).toEqual([[thread, 1]]);
295
+ expect(otherTree.threadLabels[thread]).toBe("Hidden oldest");
296
+ });
297
+
298
+ test("the route merges title fallbacks and cached labels, including hidden thread groups", async () => {
299
+ const root = await task(thread);
300
+ await task(thread);
301
+ const text = "Oldest task text. ".repeat(8);
302
+ await getDbClient().run(
303
+ "UPDATE agent_tasks SET task = ?, title = '', createdAt = ? WHERE id = ?",
304
+ [text, OLD, root.id],
305
+ );
306
+ const headers = { "x-swarm-user-id": owner.id };
307
+ const read = async () => {
308
+ const res = await fetch(`${baseUrl}/api/session-tree?show=all&showSource=ui`, { headers });
309
+ expect(res.status).toBe(200);
310
+ return (await res.json()) as { labels: Record<string, string>; sourceGroups?: string[] };
311
+ };
312
+ await rememberSessionLabel(channel, "#engineering");
313
+ expect((await read()).labels).toEqual({
314
+ [channel]: "#engineering",
315
+ [thread]: text.trim(),
316
+ });
317
+ await rememberSessionLabel(thread, "Cached thread label");
318
+ const result = await read();
319
+ expect(result.labels[thread]).toBe("Cached thread label");
320
+ expect(result.sourceGroups).toBeUndefined();
321
+ });
322
+
323
+ test("a title fallback skips the thread context and keeps the list preview shape", async () => {
324
+ const root = await task(thread);
325
+ await task(thread);
326
+ const label = async (prompt: string) => {
327
+ await getDbClient().run(
328
+ "UPDATE agent_tasks SET task = ?, title = '', createdAt = ? WHERE id = ?",
329
+ [prompt, OLD, root.id],
330
+ );
331
+ return (await getSessionTree(actor(), NOW)).threadLabels[thread];
332
+ };
333
+ const ask = "<@U1|Lena>: Fix the release notes";
334
+ expect(await label(`<thread_context>\nT: earlier message\n</thread_context>\n\n${ask}`)).toBe(
335
+ ask,
336
+ );
337
+ // A Chat SDK prompt puts its header line before the block.
338
+ const chat = await label(
339
+ "[chat] from: Lena\n\n<thread_context>\nLena: hi\n</thread_context>\n\nShip it",
340
+ );
341
+ expect(chat).toStartWith("[chat] from: Lena");
342
+ expect(chat).toEndWith("Ship it");
343
+ expect(chat).not.toContain("Lena: hi");
344
+ // A tag inside a line is text, not a block.
345
+ expect(await label("Explain the <thread_context> tag")).toBe(
346
+ "Explain the <thread_context> tag",
347
+ );
348
+ // 300 characters and "…": the list preview the UI repairs (`isListPreview`).
349
+ expect(await label(`<thread_context>\nT: hi\n</thread_context>\n\n${"a".repeat(400)}`)).toBe(
350
+ `${"a".repeat(300)}…`,
351
+ );
352
+ // A prompt that is only thread context has no label: the UI shows `Thread · <ts>`.
353
+ expect(await label("<thread_context>\nT: hi\n</thread_context>")).toBe("");
354
+ });
355
+
356
+ test("only source thread leaves nest, including Telegram and Discord", async () => {
357
+ for (const [key, expectedGroup] of [
358
+ ["shared/telegram/-100/thread:42/", "shared/telegram/-100/thread:42/"],
359
+ ["shared/discord/77/thread:99/", "shared/discord/77/thread:99/"],
360
+ ["shared/slack/c1/thread:parent/thread:🚀/", "shared/slack/c1/thread:parent/thread:🚀/"],
361
+ ["shared/slack/c1/thread:parent/child/", "shared/slack/c1/thread:parent/child/"],
362
+ ["shared/slack/c1/task:leaf/", channel],
363
+ [`personal/${owner.id}/thread:1/`, `personal/${owner.id}/`],
364
+ ["shared/notes/thread:1/", "shared/notes/"],
365
+ ]) {
366
+ await getDbClient().run("DELETE FROM agent_tasks");
367
+ const root = await task(key);
368
+ await task(key);
369
+ expect(sessionThreadGroupKey(key, NESTED_THREAD_MIN_ROOTS)).toBe(expectedGroup);
370
+ expect(sessionThreadGroupKey(key, NESTED_THREAD_MIN_ROOTS - 1)).toBe(sessionGroupKey(key));
371
+ const threadRoots = threadRootsSql();
372
+ const count = await getDbClient().get<{ roots: number }>(
373
+ `SELECT ${threadRoots.sql} AS roots FROM agent_tasks r WHERE r.id = ?`,
374
+ [...threadRoots.params, root.id],
375
+ );
376
+ expect(count?.roots).toBe(isSourceThreadKey(key) ? NESTED_THREAD_MIN_ROOTS : 0);
377
+ expect((await listRecentSessions(filters({ rootId: root.id })))[0].groupKey).toBe(
378
+ expectedGroup,
379
+ );
380
+ const tree = await getSessionTree(actor(), NOW);
381
+ const noGroup = expectedGroup === `personal/${owner.id}/`;
382
+ expect(tree.noGroup.total).toBe(noGroup ? 2 : 0);
383
+ expect(tree.groups).toEqual(
384
+ noGroup
385
+ ? []
386
+ : [
387
+ {
388
+ key: expectedGroup,
389
+ lastActivityAt: RECENT,
390
+ total: 2,
391
+ unread: 2,
392
+ running: 0,
393
+ needsYou: 0,
394
+ },
395
+ ],
396
+ );
397
+ }
398
+ });
399
+
400
+ test("channel and thread queries retain indexed key lookup without LIKE", async () => {
401
+ await task(thread);
402
+ await task(thread);
403
+ for (const groupKey of [channel, thread]) {
404
+ const client = getDbClient();
405
+ const query = spyOn(client, "query");
406
+ let call: (typeof query.mock.calls)[number];
407
+ try {
408
+ await listRecentSessions(filters({ groupKey }));
409
+ call = query.mock.calls[0]!;
410
+ } finally {
411
+ query.mockRestore();
412
+ }
413
+ const [sql, params] = call;
414
+ const plan = await client.query<{ detail: string }>(`EXPLAIN QUERY PLAN ${sql}`, params);
415
+ expect(plan.some((p) => p.detail.startsWith("SEARCH r USING INDEX"))).toBe(true);
416
+ expect(
417
+ plan.some((p) =>
418
+ p.detail.includes("SEARCH session_thread_root USING INDEX idx_agent_tasks_asset_key"),
419
+ ),
420
+ ).toBe(true);
421
+ expect(
422
+ plan.some((p) => p.detail === "SCAN r" || p.detail === "SCAN session_thread_root"),
423
+ ).toBe(false);
424
+ expect(sql).not.toContain('"key" LIKE');
425
+ expect(sql).not.toContain("INDEXED BY");
426
+ }
427
+ });
428
+ });
429
+
192
430
  describe("chain-wide read state", () => {
193
431
  test("every CASE branch uses the fixed clock and chain activity", async () => {
194
432
  const cases = [
@@ -605,6 +843,41 @@ test("session detail carries the viewer's read state with the list rule", async
605
843
  expect(bypass.lastActivityAt).toBeUndefined();
606
844
  });
607
845
 
846
+ test("session detail carries each chain task's cost, so the header can total the whole session", async () => {
847
+ const agent = await createAgent({ name: "cost-agent", isLead: false, status: "idle" });
848
+ const root = await task();
849
+ const child = await task("shared/a/", { parentTaskId: root.id });
850
+ const grandchild = await task("shared/a/", { parentTaskId: child.id });
851
+ const followUp = await task("shared/a/", { parentTaskId: root.id });
852
+ const noCost = await task("shared/a/", { parentTaskId: root.id });
853
+ const spend = async (taskId: string, totalCostUsd: number) =>
854
+ createSessionCost({
855
+ sessionId: crypto.randomUUID(),
856
+ taskId,
857
+ agentId: agent.id,
858
+ totalCostUsd,
859
+ durationMs: 1_000,
860
+ numTurns: 1,
861
+ model: "test-model",
862
+ });
863
+ await spend(root.id, 0.2);
864
+ await spend(root.id, 0.0051);
865
+ await spend(child.id, 0.5);
866
+ await spend(grandchild.id, 0.25);
867
+ await spend(followUp.id, 0.335);
868
+
869
+ const res = await fetch(`${baseUrl}/api/sessions/${root.id}`);
870
+ const body = (await res.json()) as { chain: { id: string; totalCostUsd?: number }[] };
871
+ const costs = Object.fromEntries(body.chain.map((t) => [t.id, t.totalCostUsd]));
872
+ expect(costs[root.id]).toBeCloseTo(0.2051, 6);
873
+ expect(costs[child.id]).toBeCloseTo(0.5, 6);
874
+ expect(costs[grandchild.id]).toBeCloseTo(0.25, 6);
875
+ expect(costs[followUp.id]).toBeCloseTo(0.335, 6);
876
+ expect(costs[noCost.id]).toBeUndefined();
877
+ const total = body.chain.reduce((sum, t) => sum + (t.totalCostUsd ?? 0), 0);
878
+ expect(total).toBeCloseTo(1.2901, 6);
879
+ });
880
+
608
881
  test("limit clamps to 200 and count without chain filters does not recurse", async () => {
609
882
  await getDbClient().transaction(async () => {
610
883
  for (let i = 0; i < 205; i++)
@@ -703,6 +976,67 @@ describe("source filter and list triage", () => {
703
976
  body: JSON.stringify(body),
704
977
  });
705
978
 
979
+ test("effective sources agree across lists, tree facets and triage", async () => {
980
+ const slack = await task("shared/slack/c1/thread:1/", { source: "slack" });
981
+ const mcpSlack = await task("shared/slack/c1/thread:2/", { source: "mcp" });
982
+ const telegram = await task("shared/telegram/-100/thread:42/", {
983
+ source: "chat",
984
+ contextKey: chatSdkContextKey("telegram:-100:42"),
985
+ });
986
+ const discord = await task("shared/discord/c1/task:root/", { source: "chat" });
987
+ const ui = await task("shared/", { source: "ui" });
988
+ const mcp = await task("shared/research/", { source: "mcp" });
989
+ const chat = await task("shared/research/", { source: "chat" });
990
+ const facets = (await treeOf("show=all")).sources;
991
+ expect(facets).toEqual([
992
+ { source: "slack", total: 2 },
993
+ { source: "chat", total: 1 },
994
+ { source: "discord", total: 1 },
995
+ { source: "mcp", total: 1 },
996
+ { source: "telegram", total: 1 },
997
+ { source: "ui", total: 1 },
998
+ ]);
999
+ const cases = [
1000
+ ["slack", [slack.id, mcpSlack.id]],
1001
+ ["telegram", [telegram.id]],
1002
+ ["discord", [discord.id]],
1003
+ ["mcp", [mcp.id]],
1004
+ ["ui", [ui.id]],
1005
+ ["chat", [chat.id]],
1006
+ ] as const;
1007
+ for (const [source, expected] of cases) {
1008
+ const query = `show=all&showSource=${source}`;
1009
+ const list = await listOf(`triage=inbox&${query}`);
1010
+ expect(list.sessions.map((row) => row.root.id).sort()).toEqual([...expected].sort());
1011
+ expect(list.total).toBe(expected.length);
1012
+ const tree = await treeOf(query);
1013
+ expect(tree.inbox.total).toBe(expected.length);
1014
+ expect(tree.sources).toEqual(facets);
1015
+ const res = await triage({
1016
+ action: "done",
1017
+ triage: "inbox",
1018
+ show: "all",
1019
+ showSource: [source],
1020
+ });
1021
+ expect(res.status).toBe(200);
1022
+ expect(await res.json()).toEqual({ updated: expected.length });
1023
+ const done = await listOf("triage=done&show=all");
1024
+ expect(done.sessions.map((row) => row.root.id).sort()).toEqual([...expected].sort());
1025
+ expect((await treeOf(query)).done.total).toBe(expected.length);
1026
+ const reset = await triage({ action: "inbox", triage: "done", show: "all" });
1027
+ expect(await reset.json()).toEqual({ updated: expected.length });
1028
+ }
1029
+ const hidden = await listOf("triage=inbox&show=all&hideSource=slack");
1030
+ expect(hidden.total).toBe(5);
1031
+ expect(hidden.sessions.map((row) => row.root.id)).not.toContain(mcpSlack.id);
1032
+ const hiddenTree = await treeOf("show=all&hideSource=slack");
1033
+ expect(hiddenTree.inbox.total).toBe(5);
1034
+ expect(hiddenTree.sources).toEqual(facets);
1035
+ expect(await ids({ source: ["mcp"] })).toEqual([mcp.id, mcpSlack.id].sort());
1036
+ expect(await ids({ showSource: ["slack"], hideSource: ["slack"] })).toEqual([]);
1037
+ expect((await treeOf("show=all&showSource=slack&hideSource=slack")).inbox.total).toBe(0);
1038
+ });
1039
+
706
1040
  test("hideSource leaves a source out of sections and lists, and sources still count it", async () => {
707
1041
  const slackA = await task("shared/", { source: "slack" });
708
1042
  const slackB = await task("shared/a/", { source: "slack" });
@@ -25,6 +25,7 @@ import {
25
25
  createAgent,
26
26
  createTaskExtended,
27
27
  getDbClient,
28
+ getKv,
28
29
  getLogsByTaskId,
29
30
  getSlackTasksInThread,
30
31
  getSteeringMessagesForTask,
@@ -34,7 +35,9 @@ import {
34
35
  promoteAbandonedDraftTasks,
35
36
  startTask,
36
37
  } from "../be/db";
38
+ import { installExtension } from "../be/extensions/db";
37
39
  import { MAX_TASK_ATTACHMENT_BYTES } from "../be/task-attachment-store";
40
+ import { disableExtension, enableExtension } from "../extensions/lifecycle";
38
41
  import { getFileStorageProvider, resetFileStorageProviderForTests } from "../fs/registry";
39
42
  import * as slackAppModule from "../slack/app";
40
43
  import { createAssistant } from "../slack/assistant";
@@ -49,6 +52,7 @@ import {
49
52
  setDraftLeaseRefreshMsForTests,
50
53
  } from "../slack/inbound-files";
51
54
  import { getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
55
+ import { loadBundleFixture } from "./fixtures/extensions/load";
52
56
 
53
57
  const TEST_DB_PATH = "./test-slack-inbound-files.sqlite";
54
58
  const BOT_TOKEN = "example-slack-inbound-files-token";
@@ -651,7 +655,7 @@ describe("Slack ingress with files", () => {
651
655
  "SELECT id FROM agent_tasks WHERE slackTriggerMessageTs = ?",
652
656
  [ts],
653
657
  );
654
- return { client, task: row ? await getTaskById(row.id) : null };
658
+ return { client, task: row ? await getTaskById(row.id) : null, ts };
655
659
  }
656
660
 
657
661
  test("an image with no text creates a task that carries the image (case 1)", async () => {
@@ -953,31 +957,107 @@ describe("Slack ingress with files", () => {
953
957
  });
954
958
  });
955
959
 
956
- test("a channel message that mentions the bot with a file attaches it", async () => {
960
+ for (const [route, withFiles] of [
961
+ ["lead", false],
962
+ ["lead", true],
963
+ ["no-lead", false],
964
+ ["no-lead", true],
965
+ ["follow-up", false],
966
+ ["follow-up", true],
967
+ ] as const) {
968
+ test(`Assistant DM metadata reaches hooks for ${route} with files=${withFiles}`, async () => {
969
+ const agent =
970
+ route === "no-lead"
971
+ ? undefined
972
+ : await createAgent({
973
+ name: route === "follow-up" ? "worker" : "lead",
974
+ isLead: route === "lead",
975
+ status: route === "follow-up" ? "busy" : "idle",
976
+ });
977
+ const channelId = "D0ASSISTMETADATA";
978
+ const threadTs = "1800000000.000001";
979
+ const earlier =
980
+ route === "follow-up"
981
+ ? await createTaskExtended("earlier work", {
982
+ agentId: agent!.id,
983
+ source: "slack",
984
+ slackChannelId: channelId,
985
+ slackThreadTs: threadTs,
986
+ })
987
+ : undefined;
988
+ if (earlier) await startTask(earlier.id);
989
+ const installed = await installExtension(await loadBundleFixture("record-task-create"));
990
+ const recorder = await enableExtension(installed.extension.id);
991
+ const text = "<@U_SWARM_BOT> inspect this\n keep the spacing";
992
+ try {
993
+ const { task, ts } = await sendAssistantDm({
994
+ channel: channelId,
995
+ thread_ts: threadTs,
996
+ text,
997
+ files: withFiles ? [slackFile()] : [],
998
+ });
999
+
1000
+ expect(task).not.toBeNull();
1001
+ expect(task!.agentId ?? undefined).toBe(agent?.id);
1002
+ if (earlier) expect(task!.parentTaskId).toBe(earlier.id);
1003
+ expect(await readStoredBytes(task!.id)).toEqual(withFiles ? [PNG_BYTES] : []);
1004
+ const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
1005
+ ?.value;
1006
+ expect(event).toMatchObject({
1007
+ origin: "slack",
1008
+ slack: {
1009
+ trigger: "mention",
1010
+ messages: [{ ts, userId: "U_HUMAN", text }],
1011
+ },
1012
+ });
1013
+ } finally {
1014
+ await disableExtension(recorder.id);
1015
+ }
1016
+ });
1017
+ }
1018
+
1019
+ test.each([
1020
+ false,
1021
+ true,
1022
+ ])("Slack event metadata accompanies a channel mention with files=%s", async (withFiles) => {
957
1023
  await createAgent({ name: "lead", isLead: true, status: "idle" });
1024
+ const installed = await installExtension(await loadBundleFixture("record-task-create"));
1025
+ const recorder = await enableExtension(installed.extension.id);
958
1026
  seq += 1;
959
1027
  const ts = `1900000000.${String(seq).padStart(6, "0")}`;
1028
+ try {
1029
+ await channelMessage({
1030
+ event: {
1031
+ type: "message",
1032
+ subtype: "file_share",
1033
+ channel: "C0CHANNEL1",
1034
+ ts,
1035
+ user: "U_HUMAN",
1036
+ text: "<@U_SWARM_BOT> what is this?",
1037
+ files: withFiles ? [slackFile()] : [],
1038
+ },
1039
+ body: { event_id: `evt_inbound_files_${seq}` },
1040
+ client: slackClient(),
1041
+ say: mock(async () => ({ ts: "1900000000.999999" })),
1042
+ });
960
1043
 
961
- await channelMessage({
962
- event: {
963
- type: "message",
964
- subtype: "file_share",
965
- channel: "C0CHANNEL1",
966
- ts,
967
- user: "U_HUMAN",
968
- text: "<@U_SWARM_BOT> what is this?",
969
- files: [slackFile()],
970
- },
971
- body: { event_id: `evt_inbound_files_${seq}` },
972
- client: slackClient(),
973
- say: mock(async () => ({ ts: "1900000000.999999" })),
974
- });
975
-
976
- const row = await getDbClient().get<{ id: string }>(
977
- "SELECT id FROM agent_tasks WHERE slackTriggerMessageTs = ?",
978
- [ts],
979
- );
980
- expect(row).not.toBeNull();
981
- expect(await readStoredBytes(row!.id)).toEqual([PNG_BYTES]);
1044
+ const row = await getDbClient().get<{ id: string }>(
1045
+ "SELECT id FROM agent_tasks WHERE slackTriggerMessageTs = ?",
1046
+ [ts],
1047
+ );
1048
+ expect(row).not.toBeNull();
1049
+ expect(await readStoredBytes(row!.id)).toEqual(withFiles ? [PNG_BYTES] : []);
1050
+ const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
1051
+ ?.value;
1052
+ expect(event).toMatchObject({
1053
+ origin: "slack",
1054
+ slack: {
1055
+ trigger: "mention",
1056
+ messages: [{ ts, userId: "U_HUMAN", text: "<@U_SWARM_BOT> what is this?" }],
1057
+ },
1058
+ });
1059
+ } finally {
1060
+ await disableExtension(recorder.id);
1061
+ }
982
1062
  });
983
1063
  });