@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "2.5.4",
3
+ "version": "2.7.0",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -80,6 +80,8 @@
80
80
  "sync:ai-toolbox-skills": "bun scripts/sync-ai-toolbox-skills.ts",
81
81
  "check:ai-toolbox-skills": "bun scripts/sync-ai-toolbox-skills.ts --check",
82
82
  "check:skill-sources": "bun scripts/check-skill-sources.ts",
83
+ "build:docs-index": "bun scripts/build-docs-index.ts",
84
+ "check:docs-index": "bun scripts/build-docs-index.ts --check",
83
85
  "check:operator-skill": "bun scripts/check-operator-skill.ts",
84
86
  "sync:telemetry-contract": "bun scripts/sync-telemetry-contract.ts",
85
87
  "check:telemetry-contract": "bun scripts/sync-telemetry-contract.ts --check",
@@ -185,7 +187,7 @@
185
187
  "@oxlint/plugins": "1.78.0",
186
188
  "@types/bun": "latest",
187
189
  "dependency-cruiser": "^18.0.0",
188
- "happy-dom": "^20.14.5",
190
+ "happy-dom": "^21.0.4",
189
191
  "konsistent": "1.0.0-beta.1",
190
192
  "openapi-typescript": "^7",
191
193
  "oxlint": "1.78.0",
@@ -210,7 +212,7 @@
210
212
  "@chat-adapter/teams": "4.41.1",
211
213
  "@chat-adapter/telegram": "4.41.1",
212
214
  "@cursor/sdk": "1.0.40",
213
- "@desplega.ai/agent-fs-just-bash": "0.18.5",
215
+ "@desplega.ai/agent-fs-just-bash": "0.18.9",
214
216
  "@desplega.ai/business-use": "^0.4.2",
215
217
  "@earendil-works/pi-agent-core": "^1.4.0",
216
218
  "@earendil-works/pi-ai": "^1.4.0",
@@ -0,0 +1,95 @@
1
+ import { chatThreadOfContextKey } from "../tasks/context-key";
2
+ import { normalizeAssetKey, sessionGroupKey } from "./key";
3
+
4
+ export const SESSION_SOURCES = ["slack", "telegram", "discord"] as const;
5
+ export type SessionSource = (typeof SESSION_SOURCES)[number];
6
+
7
+ /**
8
+ * The source asset key of a root session, or null when the task has no Slack
9
+ * thread and no supported Chat SDK thread. Shapes:
10
+ * - Slack fields or chat-sdk `slack:<channel>:<ts>`: `shared/slack/<channel>/thread:<ts>/`
11
+ * - chat-sdk `slack:<channel>:` (a top-level DM, empty ts): `shared/slack/<channel>/task:<id>/`
12
+ * - `telegram:<chatId>[:<topicId>]`: `shared/telegram/<chatId>/thread:<topicId>/`, else `.../task:<id>/`
13
+ * - `discord:<guild|@me>:<channelId>[:<threadId>]`: `shared/discord/<channelId>/thread:<threadId>/`, else `.../task:<id>/`
14
+ */
15
+ export function sessionSourceKey({
16
+ id,
17
+ slackChannelId,
18
+ slackThreadTs,
19
+ contextKey,
20
+ }: {
21
+ id: string;
22
+ slackChannelId?: string | null;
23
+ slackThreadTs?: string | null;
24
+ contextKey?: string | null;
25
+ }): string | null {
26
+ let parts: [SessionSource, string | undefined, string | undefined];
27
+ if (slackChannelId && slackThreadTs) {
28
+ parts = ["slack", slackChannelId, slackThreadTs];
29
+ } else {
30
+ const chat = chatThreadOfContextKey(contextKey);
31
+ if (!chat) return null;
32
+ // threadId keeps its adapter prefix, so index 0 is the adapter.
33
+ const ids = chat.threadId.split(":");
34
+ if (chat.adapter === "slack" && ids.length === 3 && !ids[2]) ids.pop();
35
+ if (ids.some((part) => !part)) return null;
36
+ if (chat.adapter === "slack" && (ids.length === 2 || ids.length === 3)) {
37
+ parts = ["slack", ids[1], ids[2]];
38
+ } else if (chat.adapter === "telegram" && (ids.length === 2 || ids.length === 3)) {
39
+ parts = ["telegram", ids[1], ids[2]];
40
+ } else if (chat.adapter === "discord" && (ids.length === 3 || ids.length === 4)) {
41
+ parts = ["discord", ids[2], ids[3]];
42
+ } else {
43
+ return null;
44
+ }
45
+ }
46
+ const [source, channel, thread] = parts;
47
+ // A "/" inside an id would add key segments and break the group shape.
48
+ if (!channel || channel.includes("/") || thread?.includes("/")) return null;
49
+ try {
50
+ return normalizeAssetKey(
51
+ `shared/${source}/${channel}/${thread ? `thread:${thread}` : `task:${id}`}/`,
52
+ );
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export function sourceOfKey(key: string): SessionSource | null {
59
+ return SESSION_SOURCES.find((source) => key.startsWith(`shared/${source}/`)) ?? null;
60
+ }
61
+
62
+ export const NESTED_THREAD_MIN_ROOTS = 2;
63
+
64
+ export function isSourceThreadKey(key: string): boolean {
65
+ return sourceOfKey(key) !== null && /\/thread:[^/]+\/$/.test(key);
66
+ }
67
+
68
+ /** A source thread with at least two owned roots gets its own nested group. */
69
+ export function sessionThreadGroupKey(key: string, threadRoots: number): string {
70
+ const normalized = normalizeAssetKey(key);
71
+ return isSourceThreadKey(normalized) && threadRoots >= NESTED_THREAD_MIN_ROOTS
72
+ ? normalized
73
+ : sessionGroupKey(normalized);
74
+ }
75
+
76
+ export const SOURCE_GROUPS_READ_ONLY = "Source groups are read-only.";
77
+
78
+ /** The source a filter matches: the source of the key prefix, else the raw task source. */
79
+ export function effectiveSource(key: string, source: string): string {
80
+ return sourceOfKey(key) ?? source;
81
+ }
82
+
83
+ /**
84
+ * Why a session root may not move to `destination` (a normalized key), or null.
85
+ * Source groups are read-only: only a move back to the root's own source key
86
+ * (Undo after a move out) may land under a source prefix. Null is No group.
87
+ */
88
+ export function sourceMoveError(
89
+ destination: string | null,
90
+ root: Parameters<typeof sessionSourceKey>[0],
91
+ ): string | null {
92
+ return destination !== null && sourceOfKey(destination) && destination !== sessionSourceKey(root)
93
+ ? SOURCE_GROUPS_READ_ONLY
94
+ : null;
95
+ }
@@ -3,6 +3,7 @@ import { getSlackConfiguration, isSlackConfigured } from "../slack/config";
3
3
  import { isEnvFlagEnabled } from "../utils/env-flag";
4
4
  import { scrubSecrets } from "../utils/secret-scrubber";
5
5
  import { getDbClient } from "./db";
6
+ import { emitRunFinished } from "./db/workflow-runs";
6
7
  import { getOAuthApp, getOAuthTokens } from "./db-queries/oauth";
7
8
  import { scrubJsonValue } from "./scrub-json";
8
9
 
@@ -446,6 +447,7 @@ export async function recordWorkflowPreflightFailure(input: {
446
447
  timestamp,
447
448
  ],
448
449
  );
450
+ emitRunFinished(runId, input.workflowId, "failed");
449
451
  // Claimed in this transaction, which also sees every child failure run
450
452
  // committed before it: concurrent failures alert exactly once a day.
451
453
  return {
@@ -68,3 +68,92 @@ export async function recordKeySeatMismatch(
68
68
  return { planChanged };
69
69
  });
70
70
  }
71
+
72
+ /** Store definitive live-test results and retire rejections for removed pool members. */
73
+ export async function recordKeyValidation(
74
+ keyType: string,
75
+ keys: Array<{
76
+ keyIndex: number;
77
+ keySuffix: string;
78
+ ok: boolean;
79
+ httpStatus?: number;
80
+ error?: string;
81
+ }>,
82
+ scope = "global",
83
+ scopeId: string | null = null,
84
+ options: { retireAbsent?: boolean } = {},
85
+ ): Promise<void> {
86
+ const effectiveScopeId = scopeId ?? "";
87
+ const provider = deriveProviderFromKeyType(keyType);
88
+ await getDbClient().transaction(async (tx) => {
89
+ const now = new Date().toISOString();
90
+ for (const key of keys) {
91
+ // Only 401 means the key is bad. A 403 on a list endpoint can be a valid
92
+ // key without that scope (an OpenAI restricted key, a gateway virtual key).
93
+ if (!key.ok && key.httpStatus !== 401) continue;
94
+ await tx.run(
95
+ `INSERT INTO api_key_status (keyType, keySuffix, keyIndex, scope, scopeId, lastValidationError, lastValidationFailedAt, provider, updatedAt)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
97
+ ON CONFLICT(keyType, keySuffix, scope, scopeId)
98
+ DO UPDATE SET lastValidationError = excluded.lastValidationError,
99
+ lastValidationFailedAt = excluded.lastValidationFailedAt,
100
+ keyIndex = excluded.keyIndex, updatedAt = excluded.updatedAt`,
101
+ [
102
+ keyType,
103
+ key.keySuffix,
104
+ key.keyIndex,
105
+ scope,
106
+ effectiveScopeId,
107
+ key.ok ? null : `HTTP ${key.httpStatus}`,
108
+ key.ok ? null : now,
109
+ provider,
110
+ now,
111
+ ],
112
+ );
113
+ // A passing test clears the key's rejection in every scope, since the
114
+ // suffix read in `getAvailableKeyIndices` matches them all.
115
+ if (key.ok) {
116
+ await tx.run(
117
+ `UPDATE api_key_status SET lastValidationError = NULL, lastValidationFailedAt = NULL, updatedAt = ?
118
+ WHERE keyType = ? AND keySuffix = ? AND lastValidationFailedAt IS NOT NULL`,
119
+ [now, keyType, key.keySuffix],
120
+ );
121
+ }
122
+ }
123
+ if (options.retireAbsent === false) return;
124
+ const placeholders = keys.map(() => "?").join(", ");
125
+ await tx.run(
126
+ `UPDATE api_key_status SET lastValidationError = NULL, lastValidationFailedAt = NULL, updatedAt = ?
127
+ WHERE keyType = ? AND scope = ? AND scopeId = ?
128
+ ${keys.length ? `AND keySuffix NOT IN (${placeholders})` : ""}
129
+ AND lastValidationFailedAt IS NOT NULL`,
130
+ [now, keyType, scope, effectiveScopeId, ...keys.map((key) => key.keySuffix)],
131
+ );
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Forget the status of a key that left its pool, and store the new index of
137
+ * each remaining key. A new key that later takes the same index or Codex slot
138
+ * then starts with no rejection, rate limit or auth bench.
139
+ */
140
+ export async function retirePoolKeyStatus(
141
+ keyType: string,
142
+ removedSuffix: string,
143
+ remaining: Array<{ keySuffix: string; keyIndex: number }>,
144
+ ): Promise<void> {
145
+ await getDbClient().transaction(async (tx) => {
146
+ if (!remaining.some((key) => key.keySuffix === removedSuffix)) {
147
+ await tx.run(
148
+ "DELETE FROM api_key_status WHERE keyType = ? AND keySuffix = ? AND scope = 'global' AND scopeId = ''",
149
+ [keyType, removedSuffix],
150
+ );
151
+ }
152
+ for (const key of remaining) {
153
+ await tx.run(
154
+ "UPDATE api_key_status SET keyIndex = ? WHERE keyType = ? AND keySuffix = ? AND scope = 'global' AND scopeId = ''",
155
+ [key.keyIndex, keyType, key.keySuffix],
156
+ );
157
+ }
158
+ });
159
+ }
@@ -0,0 +1,28 @@
1
+ import { getDbClient, upsertKv } from "../db";
2
+
3
+ const NAMESPACE = "session-labels";
4
+
5
+ export async function getSessionLabels(keys: string[]): Promise<Record<string, string>> {
6
+ if (keys.length === 0) return {};
7
+ const rows = await getDbClient().query<{ key: string; value: string }>(
8
+ `SELECT key, value FROM kv_entries
9
+ WHERE namespace = ? AND key IN (${keys.map(() => "?").join(", ")})
10
+ AND (expires_at IS NULL OR expires_at > ?)`,
11
+ [NAMESPACE, ...keys, Date.now()],
12
+ );
13
+ return Object.fromEntries(rows.map((row) => [row.key, row.value]));
14
+ }
15
+
16
+ export async function rememberSessionLabel(
17
+ key: string,
18
+ label: string,
19
+ ttlMs?: number,
20
+ ): Promise<void> {
21
+ await upsertKv({
22
+ namespace: NAMESPACE,
23
+ key,
24
+ value: label,
25
+ valueType: "string",
26
+ expiresAt: ttlMs === undefined ? null : Date.now() + ttlMs,
27
+ });
28
+ }
@@ -85,15 +85,14 @@ export async function moveTaskChainKeyInTx(
85
85
  [task.id, task.id],
86
86
  );
87
87
  // The namespace is metadata, and lastUpdatedAt tracks task activity, as in updateTaskTitle.
88
- await client.run('UPDATE agent_tasks SET "key" = ?, updated_by = ? WHERE id = ?', [
89
- key,
90
- changedBy ?? null,
91
- task.id,
92
- ]);
88
+ await client.run(
89
+ 'UPDATE agent_tasks SET "key" = ?, updated_by = COALESCE(?, updated_by) WHERE id = ?',
90
+ [key, changedBy ?? null, task.id],
91
+ );
93
92
  for (const mapping of mappedFiles) {
94
93
  if (mapping.key === key) continue;
95
94
  await client.run(
96
- 'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = ? WHERE id = ?',
95
+ 'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = COALESCE(?, updated_by) WHERE id = ?',
97
96
  [key, now, changedBy ?? null, mapping.id],
98
97
  );
99
98
  await insertAssetKeyHistory({
@@ -4,6 +4,14 @@ import {
4
4
  SESSION_LEAF_KINDS,
5
5
  sessionGroupKey,
6
6
  } from "../../assets/key";
7
+ import {
8
+ effectiveSource,
9
+ isSourceThreadKey,
10
+ NESTED_THREAD_MIN_ROOTS,
11
+ SESSION_SOURCES,
12
+ sessionThreadGroupKey,
13
+ sourceOfKey,
14
+ } from "../../assets/source-key";
7
15
  import { isTerminalTaskStatus, TERMINAL_TASK_STATUSES } from "../../types";
8
16
  import { getDbClient, literalPrefixPattern } from "../db";
9
17
  import {
@@ -14,13 +22,17 @@ import {
14
22
 
15
23
  export type SessionReadState = "unread" | "read" | "later" | "done";
16
24
  export interface SessionFilters {
17
- /** Restrict roots to these task sources. */
25
+ /** Restrict roots to these raw task sources (`agent_tasks.source`). */
18
26
  source?: string[];
19
- /** Leave out roots of these task sources (the older sessions v2 source filter). */
27
+ /**
28
+ * Leave out roots of these effective sources (the older sessions v2 source
29
+ * filter). The effective source of a root under a source key prefix such as
30
+ * `shared/slack/` is that source, else its raw task source.
31
+ */
20
32
  hideSource?: string[];
21
33
  /**
22
- * Keep only roots of these task sources (the sessions v2 source filter).
23
- * Unlike `source`, an empty list matches no root.
34
+ * Keep only roots of these effective sources (the sessions v2 source filter,
35
+ * see `hideSource`). Unlike `source`, an empty list matches no root.
24
36
  */
25
37
  showSource?: string[];
26
38
  /** Match root task text or title with a case-insensitive substring. */
@@ -74,15 +86,47 @@ function sessionReadStateSql(now = new Date().toISOString()): SqlFragment {
74
86
  };
75
87
  }
76
88
 
89
+ function sourceKeyRanges() {
90
+ return SESSION_SOURCES.map((source) => ({ source, ...keyPrefixRange(`shared/${source}/`) }));
91
+ }
92
+
93
+ /** Count every owned root at the exact source thread key, independent of viewer filters. */
94
+ export function threadRootsSql(): SqlFragment {
95
+ const key = 'r."key"';
96
+ // Trim the final slash, then all non-slash characters to locate the last segment.
97
+ // For example, shared/slack/c1/thread:1.2/ gives thread:1.2/.
98
+ const leaf = `substr(${key}, length(rtrim(rtrim(${key}, '/'), replace(${key}, '/', ''))) + 1)`;
99
+ const ranges = sourceKeyRanges();
100
+ return {
101
+ sql: `(CASE WHEN (${ranges.map(() => `(${key} >= ? AND ${key} < ?)`).join(" OR ")}) AND ${leaf} GLOB 'thread:?*/'
102
+ THEN (SELECT COUNT(*) FROM agent_tasks session_thread_root WHERE session_thread_root."key" = ${key}
103
+ AND session_thread_root.parentTaskId IS NULL AND session_thread_root.requestedByUserId IS NOT NULL)
104
+ ELSE 0 END)`,
105
+ params: ranges.flatMap(({ lo, hi }) => [lo, hi]),
106
+ };
107
+ }
108
+
109
+ /** A task's group with the list row's rule (`groupKey` on `GET /api/sessions`). */
110
+ export async function getSessionGroupKey(task: { id: string; key: string }): Promise<string> {
111
+ const threadRoots = threadRootsSql();
112
+ const row = await getDbClient().get<{ threadRoots: number }>(
113
+ `SELECT ${threadRoots.sql} AS threadRoots FROM agent_tasks r WHERE r.id = ?`,
114
+ [...threadRoots.params, task.id],
115
+ );
116
+ return sessionThreadGroupKey(task.key, row?.threadRoots ?? 0);
117
+ }
118
+
77
119
  function groupCondition(group: string): SqlFragment {
78
120
  const { lo, hi } = keyPrefixRange(group);
79
121
  // The remaining key must be one resource leaf, with no nested path.
80
122
  // SQLite substr/length count code points, so count them here too (not UTF-16 units).
81
123
  const start = [...lo].length + 1;
124
+ const threadRoots = threadRootsSql();
82
125
  return {
83
126
  sql: `(r."key" >= ? AND r."key" < ? AND (r."key" = ? OR (
84
127
  instr(substr(r."key", ?, length(r."key") - ?), '/') = 0 AND
85
- (${SESSION_LEAF_KINDS.map(() => 'substr(r."key", ?) GLOB ?').join(" OR ")})
128
+ (${SESSION_LEAF_KINDS.map(() => 'substr(r."key", ?) GLOB ?').join(" OR ")}) AND
129
+ ${threadRoots.sql} < ?
86
130
  )))`,
87
131
  params: [
88
132
  lo,
@@ -91,10 +135,21 @@ function groupCondition(group: string): SqlFragment {
91
135
  start,
92
136
  start,
93
137
  ...SESSION_LEAF_KINDS.flatMap((kind) => [start, `${kind}:?*/`]),
138
+ ...threadRoots.params,
139
+ NESTED_THREAD_MIN_ROOTS,
94
140
  ],
95
141
  };
96
142
  }
97
143
 
144
+ /** `effectiveSource` of the root `r` in SQL, with key ranges so the key index still serves it. */
145
+ function effectiveSourceSql(): SqlFragment {
146
+ const ranges = sourceKeyRanges();
147
+ return {
148
+ sql: `(CASE ${ranges.map(() => `WHEN r."key" >= ? AND r."key" < ? THEN ?`).join(" ")} ELSE r.source END)`,
149
+ params: ranges.flatMap(({ source, lo, hi }) => [lo, hi, source]),
150
+ };
151
+ }
152
+
98
153
  export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
99
154
  const conditions = ["r.parentTaskId IS NULL"];
100
155
  const params: SqlFragment["params"] = [];
@@ -107,13 +162,22 @@ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
107
162
  if (sources.length)
108
163
  add({ sql: `r.source IN (${sources.map(() => "?").join(",")})`, params: sources });
109
164
  const hidden = opts.hideSource?.filter(Boolean) ?? [];
110
- if (hidden.length)
111
- add({ sql: `r.source NOT IN (${hidden.map(() => "?").join(",")})`, params: hidden });
165
+ if (hidden.length) {
166
+ const source = effectiveSourceSql();
167
+ add({
168
+ sql: `${source.sql} NOT IN (${hidden.map(() => "?").join(",")})`,
169
+ params: [...source.params, ...hidden],
170
+ });
171
+ }
112
172
  if (opts.showSource) {
113
173
  const shown = opts.showSource.filter(Boolean);
174
+ const source = effectiveSourceSql();
114
175
  add(
115
176
  shown.length
116
- ? { sql: `r.source IN (${shown.map(() => "?").join(",")})`, params: shown }
177
+ ? {
178
+ sql: `${source.sql} IN (${shown.map(() => "?").join(",")})`,
179
+ params: [...source.params, ...shown],
180
+ }
117
181
  : { sql: "0", params: [] },
118
182
  );
119
183
  }
@@ -388,8 +452,45 @@ export interface SessionTree {
388
452
  later: SessionSectionCounts;
389
453
  done: SessionSectionCounts;
390
454
  noGroup: SessionSectionCounts;
391
- /** Each root source in this view with its session count, hidden sources included, most first. */
455
+ /** Each effective root source in this view with its session count, hidden sources included, most first. */
392
456
  sources: { source: string; total: number }[];
457
+ /** Every source group key in this view, hidden sources included, so a pane of a hidden source still gets its label. */
458
+ sourceGroups: string[];
459
+ /** Slack channel ids backed by visible roots with matching Slack fields, including hidden sources. */
460
+ slackChannels: string[];
461
+ /**
462
+ * The oldest visible root of each nested source thread, including hidden
463
+ * sources: its title, else a preview of its prompt without the thread context.
464
+ */
465
+ threadLabels: Record<string, string>;
466
+ }
467
+
468
+ /**
469
+ * The first `<thread_context>` block of a prompt, as the Slack and Chat SDK
470
+ * templates write it (each tag alone on its line). It holds the earlier thread
471
+ * messages, so a label cut from the start of the prompt never reaches the ask.
472
+ */
473
+ const THREAD_OPEN = "<thread_context>";
474
+ const THREAD_CLOSE = "</thread_context>";
475
+ const THREAD_OPEN_AT = `instr(r.task, '${THREAD_OPEN}' || char(10))`;
476
+ const THREAD_CLOSE_AT = `instr(r.task, char(10) || '${THREAD_CLOSE}')`;
477
+ /**
478
+ * The task list preview length. The UI reads this length plus "…" as a list
479
+ * preview and repairs a Slack token or a block that the cut splits
480
+ * (`isListPreview` in `apps/ui/src/lib/prompt-display.ts`).
481
+ */
482
+ const THREAD_LABEL_LENGTH = 300;
483
+ /** Extra characters for the blank lines that `threadPromptPreview` trims, so the preview stays full. */
484
+ const THREAD_LABEL_TRIM_MARGIN = 100;
485
+ /** The prompt of `r` without its first thread context block, cut for `threadPromptPreview`. */
486
+ const THREAD_PROMPT_SQL = `substr(CASE
487
+ WHEN ${THREAD_OPEN_AT} > 0 AND ${THREAD_CLOSE_AT} > ${THREAD_OPEN_AT}
488
+ THEN substr(r.task, 1, ${THREAD_OPEN_AT} - 1) || substr(r.task, ${THREAD_CLOSE_AT} + ${THREAD_CLOSE.length + 1})
489
+ ELSE r.task END, 1, ${THREAD_LABEL_LENGTH + THREAD_LABEL_TRIM_MARGIN})`;
490
+
491
+ function threadPromptPreview(prompt: string): string {
492
+ const text = prompt.trim();
493
+ return text.length > THREAD_LABEL_LENGTH ? `${text.slice(0, THREAD_LABEL_LENGTH)}…` : text;
393
494
  }
394
495
 
395
496
  export async function getSessionTree(
@@ -405,17 +506,22 @@ export async function getSessionTree(
405
506
  tree: true,
406
507
  mine: opts.mine,
407
508
  });
509
+ const threadRoots = threadRootsSql();
408
510
  const rows = await getDbClient().query<{
511
+ id: string;
409
512
  key: string;
410
513
  source: string;
514
+ slackChannelId: string | null;
411
515
  lastActivityAt: string;
412
516
  latestStatus: string;
413
517
  readState: SessionReadState | null;
414
518
  needsYou: number;
519
+ threadRoots: number;
415
520
  }>(
416
- `SELECT r."key", r.source, a.lastActivityAt, a.latestStatus, a.readState, a.needsYou
521
+ `SELECT r.id, r."key", r.source, r.slackChannelId, a.lastActivityAt, a.latestStatus, a.readState, a.needsYou,
522
+ ${threadRoots.sql} AS threadRoots
417
523
  FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id`,
418
- query.params,
524
+ [...threadRoots.params, ...query.params],
419
525
  );
420
526
  const counts = (): SessionSectionCounts => ({ total: 0, unread: 0, running: 0, needsYou: 0 });
421
527
  const result: SessionTree = {
@@ -425,10 +531,16 @@ export async function getSessionTree(
425
531
  done: counts(),
426
532
  noGroup: counts(),
427
533
  sources: [],
534
+ sourceGroups: [],
535
+ slackChannels: [],
536
+ threadLabels: {},
428
537
  };
429
538
  const hidden = new Set(opts.hideSource);
430
539
  const shown = opts.showSource ? new Set(opts.showSource) : null;
431
540
  const sources = new Map<string, number>();
541
+ const sourceGroups = new Set<string>();
542
+ const slackChannels = new Set<string>();
543
+ const threadRootIds: string[] = [];
432
544
  const groups = new Map<string, SessionTreeGroup>();
433
545
  const add = (section: SessionSectionCounts, row: (typeof rows)[number]) => {
434
546
  section.total++;
@@ -437,9 +549,20 @@ export async function getSessionTree(
437
549
  if (row.needsYou) section.needsYou++;
438
550
  };
439
551
  for (const row of rows) {
440
- sources.set(row.source, (sources.get(row.source) ?? 0) + 1);
441
- if (hidden.has(row.source) || (shown && !shown.has(row.source))) continue;
442
- const key = sessionGroupKey(row.key);
552
+ const source = effectiveSource(row.key, row.source);
553
+ sources.set(source, (sources.get(source) ?? 0) + 1);
554
+ const key = sessionThreadGroupKey(row.key, row.threadRoots);
555
+ const sourceGroupKey = sessionGroupKey(row.key);
556
+ const channelId = /^shared\/slack\/([^/]+)\/$/.exec(sourceGroupKey)?.[1];
557
+ if (channelId && row.slackChannelId?.toLowerCase() === channelId) {
558
+ slackChannels.add(channelId);
559
+ }
560
+ if (row.threadRoots >= NESTED_THREAD_MIN_ROOTS && isSourceThreadKey(row.key)) {
561
+ threadRootIds.push(row.id);
562
+ sourceGroups.add(sourceGroupKey);
563
+ }
564
+ if (sourceOfKey(key)) sourceGroups.add(key);
565
+ if (hidden.has(source) || (shown && !shown.has(source))) continue;
443
566
  const noGroup =
444
567
  key === "shared/" || (actor.kind === "user" && key === `personal/${actor.userId}/`);
445
568
  let group: SessionTreeGroup | undefined;
@@ -462,6 +585,22 @@ export async function getSessionTree(
462
585
  result.sources = [...sources]
463
586
  .map(([source, total]) => ({ source, total }))
464
587
  .sort((a, b) => b.total - a.total || a.source.localeCompare(b.source));
588
+ result.sourceGroups = [...sourceGroups].sort();
589
+ result.slackChannels = [...slackChannels].sort();
590
+ if (threadRootIds.length) {
591
+ // A missing or empty title falls back to a preview of the prompt without its thread context.
592
+ const labels = await getDbClient().query<{ key: string; title: string | null; prompt: string }>(
593
+ `SELECT "key", title, prompt FROM (
594
+ SELECT r."key", NULLIF(trim(r.title), '') AS title, ${THREAD_PROMPT_SQL} AS prompt,
595
+ ROW_NUMBER() OVER (PARTITION BY r."key" ORDER BY r.createdAt, r.rowid) AS ordinal
596
+ FROM agent_tasks r WHERE r.id IN (${threadRootIds.map(() => "?").join(",")})
597
+ ) WHERE ordinal = 1`,
598
+ threadRootIds,
599
+ );
600
+ result.threadLabels = Object.fromEntries(
601
+ labels.map((row) => [row.key, row.title ?? threadPromptPreview(row.prompt)]),
602
+ );
603
+ }
465
604
  return result;
466
605
  }
467
606
 
@@ -803,6 +803,7 @@ export interface TaskFilters {
803
803
  taskType?: string;
804
804
  tags?: string[];
805
805
  scheduleId?: string;
806
+ workflowRunId?: string;
806
807
  /** Exact canonical asset namespace. */
807
808
  key?: string;
808
809
  /** Canonical namespace subtree prefix. */
@@ -937,6 +938,11 @@ export async function getAllTasks(
937
938
  params.push(filters.scheduleId);
938
939
  }
939
940
 
941
+ if (filters?.workflowRunId) {
942
+ conditions.push("workflowRunId = ?");
943
+ params.push(filters.workflowRunId);
944
+ }
945
+
940
946
  if (filters?.key) {
941
947
  conditions.push('"key" = ?');
942
948
  params.push(normalizeAssetKey(filters.key));
@@ -1103,6 +1109,11 @@ export async function getTasksCount(
1103
1109
  params.push(filters.scheduleId);
1104
1110
  }
1105
1111
 
1112
+ if (filters?.workflowRunId) {
1113
+ conditions.push("workflowRunId = ?");
1114
+ params.push(filters.workflowRunId);
1115
+ }
1116
+
1106
1117
  if (filters?.key) {
1107
1118
  conditions.push('"key" = ?');
1108
1119
  params.push(normalizeAssetKey(filters.key));
@@ -1,5 +1,6 @@
1
1
  import pkg from "../../../../package.json";
2
2
  import { defaultAssetKey } from "../../../assets/key";
3
+ import { sessionSourceKey } from "../../../assets/source-key";
3
4
  import type { TaskTelemetryEvent, TaskTelemetryInput } from "../../../telemetry-trigger";
4
5
  import type {
5
6
  Agent,
@@ -89,7 +90,11 @@ export async function createTask(
89
90
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
90
91
  [
91
92
  id,
92
- defaultAssetKey("task", id),
93
+ sessionSourceKey({
94
+ id,
95
+ slackChannelId: options?.slackChannelId,
96
+ slackThreadTs: options?.slackThreadTs,
97
+ }) ?? defaultAssetKey("task", id),
93
98
  agentId,
94
99
  scrubSecrets(task),
95
100
  "pending",