@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
@@ -0,0 +1,429 @@
1
+ import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
2
+ import { defaultAssetKey } from "../assets/key";
3
+ import { sessionSourceKey } from "../assets/source-key";
4
+ import * as audit from "../be/asset-key-audit";
5
+ import {
6
+ closeDb,
7
+ createTaskExtended,
8
+ createUser,
9
+ getDbClient,
10
+ getTaskById,
11
+ initDb,
12
+ insertTaskAttachment,
13
+ listRecentSessions,
14
+ upsertInboxState,
15
+ } from "../be/db";
16
+ import * as sessionMoves from "../be/db/session-moves";
17
+ import type { DbParam } from "../be/db-client";
18
+ import { runSessionSourceBackfill } from "../be/session-source-backfill";
19
+ import { chatSdkContextKey } from "../tasks/context-key";
20
+
21
+ const DONE_KEY = "session-source-keys-v1";
22
+ const CURSOR_KEY = `${DONE_KEY}:cursor`;
23
+ const BEFORE = "2026-10-21T10:30:00.000Z";
24
+ const DONE_AT = "2026-10-21T11:00:00.000Z";
25
+ const slack = { slackChannelId: "C0AR967K0KZ", slackThreadTs: "1729000000.123456" };
26
+ let owner: string;
27
+
28
+ beforeEach(async () => {
29
+ initDb(":memory:");
30
+ owner = (await createUser({ name: "Backfill owner" })).id;
31
+ });
32
+ afterEach(() => closeDb());
33
+
34
+ // SQL preserves pre-source-key rows, including uppercase legacy task ids.
35
+ async function seedTask(
36
+ opts: {
37
+ id?: string;
38
+ key?: string;
39
+ source?: string;
40
+ parentTaskId?: string;
41
+ requestedByUserId?: string | null;
42
+ slackChannelId?: string;
43
+ slackThreadTs?: string;
44
+ contextKey?: string;
45
+ } = {},
46
+ ) {
47
+ const id = opts.id ?? crypto.randomUUID();
48
+ await getDbClient().run(
49
+ `INSERT INTO agent_tasks
50
+ (id, task, status, source, parentTaskId, requestedByUserId, "key",
51
+ slackChannelId, slackThreadTs, contextKey, createdAt, lastUpdatedAt, updated_by)
52
+ VALUES (?, 'Old session', 'completed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
53
+ [
54
+ id,
55
+ opts.source ?? "slack",
56
+ opts.parentTaskId ?? null,
57
+ opts.requestedByUserId === undefined ? owner : opts.requestedByUserId,
58
+ opts.key ?? defaultAssetKey("task", id),
59
+ opts.slackChannelId ?? null,
60
+ opts.slackThreadTs ?? null,
61
+ opts.contextKey ?? null,
62
+ BEFORE,
63
+ BEFORE,
64
+ owner,
65
+ ],
66
+ );
67
+ return (await getTaskById(id))!;
68
+ }
69
+
70
+ async function seedState(key: string) {
71
+ return getDbClient().get<{ seededHash: string }>(
72
+ "SELECT seededHash FROM seed_state WHERE kind = 'maintenance' AND key = ?",
73
+ [key],
74
+ );
75
+ }
76
+
77
+ test("moves both old defaults, whole chains and mapped files with history, without changing read or Done state", async () => {
78
+ const roots = [
79
+ await seedTask({ ...slack, key: "shared/" }),
80
+ await seedTask({
81
+ ...slack,
82
+ slackThreadTs: "1729000000.654321",
83
+ id: crypto.randomUUID().toUpperCase(),
84
+ }),
85
+ ];
86
+ const expectedHistory: unknown[] = [];
87
+ const tasks = roots.map((task) => ({ task, key: sessionSourceKey(task) }));
88
+ const mappings: { id: string; key: string | null }[] = [];
89
+ for (const [index, root] of roots.entries()) {
90
+ const child = await seedTask({ parentTaskId: root.id, key: root.key, source: "mcp" });
91
+ const grandchild = await seedTask({
92
+ parentTaskId: child.id,
93
+ key: "shared/other-child-key/",
94
+ source: "mcp",
95
+ });
96
+ tasks.push(...[child, grandchild].map((task) => ({ task, key: sessionSourceKey(root) })));
97
+ const attachment = await insertTaskAttachment({
98
+ taskId: grandchild.id,
99
+ kind: "agent-fs",
100
+ name: "notes",
101
+ path: `backfill/${grandchild.id}`,
102
+ providerId: "agent-fs",
103
+ providerKey: `backfill/${grandchild.id}`,
104
+ });
105
+ const mapping = await getDbClient().get<{ id: string; key: string }>(
106
+ "SELECT id, key FROM asset_key_mappings WHERE source_entity_id = ?",
107
+ [attachment.id],
108
+ );
109
+ expect(mapping).not.toBeNull();
110
+ mappings.push({ id: mapping!.id, key: sessionSourceKey(root) });
111
+ await getDbClient().run("UPDATE asset_key_mappings SET updated_by = ? WHERE id = ?", [
112
+ owner,
113
+ mapping!.id,
114
+ ]);
115
+ for (const task of [root, child, grandchild]) {
116
+ expectedHistory.push({
117
+ entity_type: "task",
118
+ entity_id: task.id,
119
+ previous_key: task.key,
120
+ new_key: sessionSourceKey(root),
121
+ changed_by: null,
122
+ });
123
+ }
124
+ expectedHistory.push({
125
+ entity_type: "file",
126
+ entity_id: mapping!.id,
127
+ previous_key: mapping!.key,
128
+ new_key: sessionSourceKey(root),
129
+ changed_by: null,
130
+ });
131
+ await upsertInboxState({
132
+ userId: owner,
133
+ itemType: "to_read",
134
+ itemId: root.id,
135
+ status: index === 0 ? "done" : "open",
136
+ doneAt: index === 0 ? DONE_AT : undefined,
137
+ readAt: BEFORE,
138
+ lastReadAt: BEFORE,
139
+ });
140
+ }
141
+ const inboxBefore = await getDbClient().query("SELECT * FROM inbox_item_state ORDER BY itemId");
142
+ const sessionsBefore = await listRecentSessions({ viewer: { userId: owner, scope: "all" } });
143
+ expect(sessionsBefore.find((s) => s.root.id === roots[0]!.id)?.readState).toBe("done");
144
+ const historyBefore = await getDbClient().query(
145
+ "SELECT entity_type, entity_id, previous_key, new_key, changed_by FROM asset_key_history",
146
+ );
147
+ const auditSpy = spyOn(audit, "auditAssetKeys");
148
+ try {
149
+ expect(await runSessionSourceBackfill({ batchSize: 1 })).toEqual({
150
+ scanned: 2,
151
+ moved: 2,
152
+ skipped: 0,
153
+ failed: 0,
154
+ });
155
+ expect(auditSpy).toHaveBeenCalledTimes(1);
156
+ } finally {
157
+ auditSpy.mockRestore();
158
+ }
159
+ for (const { task, key } of tasks) {
160
+ const after = await getTaskById(task.id);
161
+ expect(after?.key).toBe(key);
162
+ expect(after?.lastUpdatedAt).toBe(task.lastUpdatedAt);
163
+ expect(
164
+ await getDbClient().get("SELECT updated_by FROM agent_tasks WHERE id = ?", [task.id]),
165
+ ).toEqual({ updated_by: owner });
166
+ }
167
+ for (const { id, key } of mappings) {
168
+ expect(
169
+ await getDbClient().get("SELECT key, updated_by FROM asset_key_mappings WHERE id = ?", [id]),
170
+ ).toEqual({ key, updated_by: owner });
171
+ }
172
+ const history = await getDbClient().query(
173
+ "SELECT entity_type, entity_id, previous_key, new_key, changed_by FROM asset_key_history",
174
+ );
175
+ expect(history).toHaveLength(historyBefore.length + expectedHistory.length);
176
+ expect(history).toEqual(expect.arrayContaining([...historyBefore, ...expectedHistory]));
177
+ expect(await getDbClient().query("SELECT * FROM inbox_item_state ORDER BY itemId")).toEqual(
178
+ inboxBefore,
179
+ );
180
+ const sessionsAfter = await listRecentSessions({ viewer: { userId: owner, scope: "all" } });
181
+ for (const before of sessionsBefore) {
182
+ expect(sessionsAfter.find((s) => s.root.id === before.root.id)?.readState).toBe(
183
+ before.readState,
184
+ );
185
+ }
186
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
187
+ expect(await seedState(CURSOR_KEY)).toBeNull();
188
+ });
189
+
190
+ test("preserves user keys and unsupported roots, and moves mcp and ownerless Slack roots", async () => {
191
+ const custom = await seedTask({ ...slack, key: "shared/research/" });
192
+ const child = await seedTask({ ...slack, parentTaskId: custom.id, key: "shared/" });
193
+ const foreignDefault = await seedTask({ ...slack, key: defaultAssetKey("task", custom.id) });
194
+ const teams = await seedTask({ source: "chat", contextKey: chatSdkContextKey("teams:c:t") });
195
+ const ordinary = await seedTask({ source: "ui" });
196
+ const missingThread = await seedTask({ slackChannelId: slack.slackChannelId });
197
+ const mcp = await seedTask({ ...slack, source: "mcp" });
198
+ const ownerless = await seedTask({ ...slack, requestedByUserId: null });
199
+ expect(await runSessionSourceBackfill()).toEqual({ scanned: 3, moved: 2, skipped: 1, failed: 0 });
200
+ for (const task of [custom, child, foreignDefault, teams, ordinary, missingThread]) {
201
+ expect(await getTaskById(task.id)).toEqual(task);
202
+ }
203
+ for (const task of [mcp, ownerless]) {
204
+ expect((await getTaskById(task.id))?.key).toBe(sessionSourceKey(task));
205
+ }
206
+ expect((await getTaskById(ownerless.id))?.requestedByUserId).toBeUndefined();
207
+ });
208
+
209
+ test("moves supported chat roots", async () => {
210
+ const threads = [
211
+ ["telegram:-1001234567890:42", "shared/telegram/-1001234567890/thread:42/"],
212
+ ["telegram:555", "shared/telegram/555/task:00000000-0000-4000-8000-000000000001/"],
213
+ ["discord:guild:channel:thread", "shared/discord/channel/thread:thread/"],
214
+ ["discord:@me:dm", "shared/discord/dm/task:00000000-0000-4000-8000-000000000003/"],
215
+ ["slack:C123:1.2", "shared/slack/c123/thread:1.2/"],
216
+ ["slack:D123:", "shared/slack/d123/task:00000000-0000-4000-8000-000000000005/"],
217
+ ];
218
+ const roots = [];
219
+ for (const [index, [thread, key]] of threads.entries()) {
220
+ const root = await seedTask({
221
+ id: `00000000-0000-4000-8000-00000000000${index}`,
222
+ source: "chat",
223
+ contextKey: chatSdkContextKey(thread!),
224
+ });
225
+ roots.push({ root, key });
226
+ }
227
+ const invalid = await seedTask({ ...slack, slackChannelId: "x".repeat(255) });
228
+ expect(await runSessionSourceBackfill()).toEqual({ scanned: 7, moved: 6, skipped: 1, failed: 0 });
229
+ for (const { root, key } of roots) {
230
+ expect((await getTaskById(root.id))?.key).toBe(key);
231
+ }
232
+ expect(await getTaskById(invalid.id)).toEqual(invalid);
233
+ });
234
+
235
+ test("a completed run does not scan or audit again, even if another old root appears", async () => {
236
+ await seedTask(slack);
237
+ await runSessionSourceBackfill();
238
+ const before = await getDbClient().query("SELECT * FROM asset_key_history ORDER BY id");
239
+ const later = await seedTask(slack);
240
+ const auditSpy = spyOn(audit, "auditAssetKeys");
241
+ try {
242
+ expect(await runSessionSourceBackfill()).toBeNull();
243
+ expect(auditSpy).not.toHaveBeenCalled();
244
+ } finally {
245
+ auditSpy.mockRestore();
246
+ }
247
+ expect(await getTaskById(later.id)).toEqual(later);
248
+ expect(await getDbClient().query("SELECT * FROM asset_key_history ORDER BY id")).toEqual(before);
249
+ });
250
+
251
+ test("isolates a failed root and retries it from the start on the next run", async () => {
252
+ const roots = [];
253
+ for (let index = 0; index < 3; index++) {
254
+ roots.push(await seedTask({ ...slack, id: `00000000-0000-4000-8000-00000000000${index}` }));
255
+ }
256
+ const move = sessionMoves.moveTaskChainKeyInTx;
257
+ const moveSpy = spyOn(sessionMoves, "moveTaskChainKeyInTx").mockImplementationOnce(
258
+ async (...args) => {
259
+ await move(...args);
260
+ throw new Error("Root move failed");
261
+ },
262
+ );
263
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
264
+ try {
265
+ expect(await runSessionSourceBackfill({ batchSize: 1 })).toEqual({
266
+ scanned: 3,
267
+ moved: 2,
268
+ skipped: 0,
269
+ failed: 1,
270
+ });
271
+ expect(moveSpy).toHaveBeenCalledTimes(3);
272
+ expect(moveSpy.mock.calls[0]?.[0]).toBe(roots[0]!.id);
273
+ expect(warn).toHaveBeenCalledTimes(2);
274
+ expect(warn.mock.calls[0]?.[0]).toContain(`root=${roots[0]!.id} failed: Root move failed`);
275
+ expect(await getTaskById(roots[0]!.id)).toEqual(roots[0]!);
276
+ for (const root of roots.slice(1)) {
277
+ expect((await getTaskById(root.id))?.key).toBe(sessionSourceKey(root));
278
+ }
279
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(2);
280
+ expect(await seedState(DONE_KEY)).toBeNull();
281
+ expect(await seedState(CURSOR_KEY)).toBeNull();
282
+ } finally {
283
+ moveSpy.mockRestore();
284
+ warn.mockRestore();
285
+ }
286
+ expect(await runSessionSourceBackfill()).toEqual({ scanned: 1, moved: 1, skipped: 0, failed: 0 });
287
+ expect((await getTaskById(roots[0]!.id))?.key).toBe(sessionSourceKey(roots[0]!));
288
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(3);
289
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
290
+ expect(await seedState(CURSOR_KEY)).toBeNull();
291
+ });
292
+
293
+ test("preserves a user move and includes a child created after the batch read", async () => {
294
+ const movedByUser = await seedTask(slack);
295
+ const root = await seedTask(slack);
296
+ const client = getDbClient();
297
+ const query = client.query.bind(client);
298
+ let childId: string | undefined;
299
+ const querySpy = spyOn(client, "query").mockImplementation(
300
+ async <T>(sql: string, params?: DbParam[]): Promise<T[]> => {
301
+ const rows = await query<T>(sql, params);
302
+ if (sql.startsWith("SELECT id, slackChannelId") && rows.length > 0) {
303
+ await client.run('UPDATE agent_tasks SET "key" = ? WHERE id = ?', [
304
+ "shared/research/",
305
+ movedByUser.id,
306
+ ]);
307
+ childId = (await seedTask({ parentTaskId: root.id, key: root.key, source: "mcp" })).id;
308
+ }
309
+ return rows;
310
+ },
311
+ );
312
+ try {
313
+ expect(await runSessionSourceBackfill()).toEqual({
314
+ scanned: 2,
315
+ moved: 1,
316
+ skipped: 1,
317
+ failed: 0,
318
+ });
319
+ expect(childId).toBeDefined();
320
+ } finally {
321
+ querySpy.mockRestore();
322
+ }
323
+ expect((await getTaskById(movedByUser.id))?.key).toBe("shared/research/");
324
+ expect((await getTaskById(root.id))?.key).toBe(sessionSourceKey(root));
325
+ expect((await getTaskById(childId!))?.key).toBe(sessionSourceKey(root));
326
+ });
327
+
328
+ test("a child whose parent moves after the parent read still inherits the moved key", async () => {
329
+ const root = await seedTask(slack);
330
+ const client = getDbClient();
331
+ const transaction = client.transaction.bind(client);
332
+ let backfilled = false;
333
+ // The first transaction is the child's INSERT: the backfill commits first.
334
+ const txSpy = spyOn(client, "transaction").mockImplementationOnce(async (fn, opts) => {
335
+ txSpy.mockRestore();
336
+ backfilled = true;
337
+ expect(await runSessionSourceBackfill()).toMatchObject({ moved: 1, failed: 0 });
338
+ return transaction(fn, opts);
339
+ });
340
+ let child: Awaited<ReturnType<typeof createTaskExtended>>;
341
+ try {
342
+ child = await createTaskExtended("Child", { parentTaskId: root.id });
343
+ } finally {
344
+ txSpy.mockRestore();
345
+ }
346
+ expect(backfilled).toBe(true);
347
+ expect((await getTaskById(root.id))?.key).toBe(sessionSourceKey(root));
348
+ expect(child.key).toBe(sessionSourceKey(root));
349
+ });
350
+
351
+ test("resumes after a committed cursor, including skipped roots, when afterBatch throws once", async () => {
352
+ const roots = [
353
+ await seedTask({
354
+ id: "00000000-0000-4000-8000-000000000001",
355
+ source: "chat",
356
+ contextKey: chatSdkContextKey("teams:c:t"),
357
+ }),
358
+ await seedTask({ id: "00000000-0000-4000-8000-000000000002", ...slack }),
359
+ await seedTask({ id: "00000000-0000-4000-8000-000000000003", ...slack }),
360
+ await seedTask({ id: "00000000-0000-4000-8000-000000000004", ...slack }),
361
+ await seedTask({ id: "00000000-0000-4000-8000-000000000005", ...slack }),
362
+ ];
363
+ let firstBatch: string[] = [];
364
+ await expect(
365
+ runSessionSourceBackfill({
366
+ batchSize: 2,
367
+ afterBatch: ({ ids }) => {
368
+ firstBatch = ids;
369
+ throw new Error("Interrupted after batch 1");
370
+ },
371
+ }),
372
+ ).rejects.toThrow("Interrupted after batch 1");
373
+ expect(firstBatch).toEqual(roots.slice(0, 2).map((r) => r.id));
374
+ expect(await seedState(CURSOR_KEY)).toEqual({ seededHash: roots[1]!.id });
375
+ expect(await seedState(DONE_KEY)).toBeNull();
376
+ expect((await getTaskById(roots[1]!.id))?.key).toBe(sessionSourceKey(roots[1]!));
377
+ expect((await getTaskById(roots[2]!.id))?.key).toBe(roots[2]!.key);
378
+ const resumed: string[] = [];
379
+ expect(
380
+ await runSessionSourceBackfill({
381
+ batchSize: 2,
382
+ afterBatch: ({ ids }) => {
383
+ resumed.push(...ids);
384
+ },
385
+ }),
386
+ ).toEqual({ scanned: 3, moved: 3, skipped: 0, failed: 0 });
387
+ expect(resumed).toEqual(roots.slice(2).map((r) => r.id));
388
+ expect(await getTaskById(roots[0]!.id)).toEqual(roots[0]!);
389
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(4);
390
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
391
+ expect(await seedState(CURSOR_KEY)).toBeNull();
392
+ });
393
+
394
+ test("a blocked audit warns without a marker or cursor, then retries successfully", async () => {
395
+ const root = await seedTask(slack);
396
+ const originalAudit = audit.auditAssetKeys;
397
+ const auditSpy = spyOn(audit, "auditAssetKeys").mockImplementation((db) => ({
398
+ ...originalAudit(db),
399
+ ok: false,
400
+ warningCount: 1,
401
+ }));
402
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
403
+ try {
404
+ expect(await runSessionSourceBackfill()).toBeNull();
405
+ expect(auditSpy).toHaveBeenCalledTimes(1);
406
+ expect(warn).toHaveBeenCalledTimes(1);
407
+ expect(await seedState(DONE_KEY)).toBeNull();
408
+ expect(await seedState(CURSOR_KEY)).toBeNull();
409
+ expect(await getTaskById(root.id)).toEqual(root);
410
+ expect(await getDbClient().query("SELECT id FROM asset_key_history")).toHaveLength(0);
411
+ } finally {
412
+ auditSpy.mockRestore();
413
+ warn.mockRestore();
414
+ }
415
+ expect((await runSessionSourceBackfill())?.moved).toBe(1);
416
+ expect(await seedState(DONE_KEY)).toEqual({ seededHash: "done" });
417
+ });
418
+
419
+ test("unexpected audit errors propagate without a marker", async () => {
420
+ const auditSpy = spyOn(audit, "auditAssetKeys").mockImplementation(() => {
421
+ throw new TypeError("Unexpected audit failure");
422
+ });
423
+ try {
424
+ await expect(runSessionSourceBackfill()).rejects.toThrow("Unexpected audit failure");
425
+ expect(await seedState(DONE_KEY)).toBeNull();
426
+ } finally {
427
+ auditSpy.mockRestore();
428
+ }
429
+ });
@@ -0,0 +1,236 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ import { defaultAssetKey } from "../assets/key";
5
+ import {
6
+ effectiveSource,
7
+ SESSION_SOURCES,
8
+ sessionSourceKey,
9
+ sourceMoveError,
10
+ sourceOfKey,
11
+ } from "../assets/source-key";
12
+ import { closeDb, createAgent, createTask, createTaskExtended, initDb } from "../be/db";
13
+ import { chatSdkContextKey } from "../tasks/context-key";
14
+ import { registerSendTaskTool } from "../tools/send-task";
15
+
16
+ const slack = { slackChannelId: "C0AR967K0KZ", slackThreadTs: "1729000000.123456" };
17
+ const slackKey = "shared/slack/c0ar967k0kz/thread:1729000000.123456/";
18
+
19
+ test("effective source uses a complete source prefix or retains the raw source", () => {
20
+ for (const source of SESSION_SOURCES) {
21
+ expect(effectiveSource(`shared/${source}/channel/thread:1/`, "mcp")).toBe(source);
22
+ expect(effectiveSource(`shared/${source}-other/`, "chat")).toBe("chat");
23
+ }
24
+ expect(effectiveSource("shared/research/", "slack")).toBe("slack");
25
+ expect(effectiveSource("shared/research/", "chat")).toBe("chat");
26
+ });
27
+
28
+ test("source move guard permits only exact own source keys under source prefixes", () => {
29
+ const root = { id: "root", ...slack };
30
+ for (const destination of [null, "shared/research/", "shared/slack-other/", slackKey]) {
31
+ expect(sourceMoveError(destination, root)).toBeNull();
32
+ }
33
+ for (const destination of [
34
+ "shared/slack/",
35
+ "shared/slack/c0ar967k0kz/",
36
+ "shared/slack/c0ar967k0kz/thread:other/",
37
+ "shared/telegram/123/",
38
+ "shared/discord/c1/",
39
+ ]) {
40
+ expect(sourceMoveError(destination, root)).toBe("Source groups are read-only.");
41
+ }
42
+ expect(sourceMoveError(slackKey, { id: "other" })).toBe("Source groups are read-only.");
43
+ const chatRoot = { id: "root", contextKey: chatSdkContextKey("telegram:-100") };
44
+ expect(sourceMoveError("shared/telegram/-100/task:root/", chatRoot)).toBeNull();
45
+ expect(sourceMoveError("shared/telegram/-100/", chatRoot)).toBe("Source groups are read-only.");
46
+ });
47
+
48
+ describe("session source key shapes", () => {
49
+ test("Slack fields produce a lowercase channel key", () => {
50
+ expect(sessionSourceKey({ id: "root", ...slack })).toBe(slackKey);
51
+ });
52
+
53
+ test("Slack DMs use the same thread shape", () => {
54
+ expect(sessionSourceKey({ id: "root", ...slack, slackChannelId: "D123" })).toBe(
55
+ "shared/slack/d123/thread:1729000000.123456/",
56
+ );
57
+ });
58
+
59
+ test.each([
60
+ ["slack:C123:1729000000.123456", "shared/slack/c123/thread:1729000000.123456/"],
61
+ ["telegram:123:42", "shared/telegram/123/thread:42/"],
62
+ ["telegram:123", "shared/telegram/123/task:root/"],
63
+ ["telegram:-1001234567890:42", "shared/telegram/-1001234567890/thread:42/"],
64
+ ["telegram:-1001234567890", "shared/telegram/-1001234567890/task:root/"],
65
+ ["discord:guild:channel:thread", "shared/discord/channel/thread:thread/"],
66
+ ["discord:guild:channel", "shared/discord/channel/task:root/"],
67
+ ["discord:@me:dm:thread", "shared/discord/dm/thread:thread/"],
68
+ ["discord:@me:dm", "shared/discord/dm/task:root/"],
69
+ ["teams:conversation:thread", null],
70
+ ["other:thread", null],
71
+ ["slack:D123:", "shared/slack/d123/task:root/"],
72
+ ["slack:D123", "shared/slack/d123/task:root/"],
73
+ ["slack:C123::", null],
74
+ ["telegram:chat:", null],
75
+ ["discord:guild", null],
76
+ ["discord:guild:a/b:thread", null],
77
+ ])("chat-sdk %s", (threadId, expected) => {
78
+ expect(
79
+ sessionSourceKey({ id: "root", contextKey: chatSdkContextKey(threadId as string) }),
80
+ ).toBe(expected);
81
+ });
82
+
83
+ test("Slack fields take precedence over a chat context", () => {
84
+ expect(
85
+ sessionSourceKey({
86
+ id: "root",
87
+ ...slack,
88
+ contextKey: chatSdkContextKey("telegram:123:42"),
89
+ }),
90
+ ).toBe(slackKey);
91
+ });
92
+
93
+ test("missing or malformed context produces no source key", () => {
94
+ for (const contextKey of [undefined, null, "", "task:chat-sdk:telegram:%ZZ", "task:ui:abc"]) {
95
+ expect(sessionSourceKey({ id: "root", contextKey })).toBeNull();
96
+ }
97
+ expect(sessionSourceKey({ id: "root", slackChannelId: "C123" })).toBeNull();
98
+ expect(sessionSourceKey({ id: "root", slackThreadTs: "1.2" })).toBeNull();
99
+ });
100
+
101
+ test("normalization failures produce no source key", () => {
102
+ for (const slackChannelId of ["x".repeat(255), "..", "bad\\channel", "bad\0channel", "a/b"]) {
103
+ expect(sessionSourceKey({ id: "root", ...slack, slackChannelId })).toBeNull();
104
+ }
105
+ expect(
106
+ sessionSourceKey({
107
+ id: "root",
108
+ contextKey: chatSdkContextKey(`telegram:${"x".repeat(255)}`),
109
+ }),
110
+ ).toBeNull();
111
+ });
112
+
113
+ test("source prefixes require the complete source segment", () => {
114
+ for (const source of SESSION_SOURCES) {
115
+ expect(sourceOfKey(`shared/${source}/`)).toBe(source);
116
+ expect(sourceOfKey(`shared/${source}/channel/thread:123/`)).toBe(source);
117
+ expect(sourceOfKey(`shared/${source}-other/`)).toBeNull();
118
+ }
119
+ for (const key of ["shared/", "shared/slack", "personal/user/slack/", "shared/task:root/"]) {
120
+ expect(sourceOfKey(key)).toBeNull();
121
+ }
122
+ });
123
+ });
124
+
125
+ describe("session source keys at insert", () => {
126
+ beforeAll(() => initDb(":memory:"));
127
+ afterAll(() => closeDb());
128
+
129
+ test.each([
130
+ "slack",
131
+ "mcp",
132
+ ] as const)("%s roots with Slack fields get a source key", async (source) => {
133
+ const root = await createTaskExtended("Slack root", { source, ...slack });
134
+ expect(root.key).toBe(slackKey);
135
+ });
136
+
137
+ test("a child inherits its Slack root key", async () => {
138
+ const root = await createTaskExtended("Slack root", { source: "slack", ...slack });
139
+ const child = await createTaskExtended("Child", { parentTaskId: root.id });
140
+ expect(child.key).toBe(root.key);
141
+ });
142
+
143
+ test("send-task permits a child to inherit its Slack root key", async () => {
144
+ const agent = await createAgent({ name: "Source caller", status: "idle", maxTasks: 10 });
145
+ const target = await createAgent({ name: "Source target", status: "idle", maxTasks: 10 });
146
+ const root = await createTaskExtended("Slack root", {
147
+ agentId: agent.id,
148
+ source: "slack",
149
+ ...slack,
150
+ });
151
+ const server = new McpServer({ name: "source-key-test", version: "1.0.0" });
152
+ registerSendTaskTool(server);
153
+ const tools = (
154
+ server as unknown as {
155
+ _registeredTools: Record<
156
+ string,
157
+ {
158
+ handler: (args: unknown, extra: unknown) => Promise<CallToolResult>;
159
+ }
160
+ >;
161
+ }
162
+ )._registeredTools;
163
+ const result = await tools["send-task"]!.handler(
164
+ {
165
+ agentId: target.id,
166
+ routingReason: "human_pinned",
167
+ routingNote: "The test selects the target worker.",
168
+ task: "Child in a Slack source group",
169
+ parentTaskId: root.id,
170
+ allowDuplicate: true,
171
+ },
172
+ { requestInfo: { headers: { "x-agent-id": agent.id, "x-source-task-id": root.id } } },
173
+ );
174
+ expect(result.structuredContent).toMatchObject({
175
+ success: true,
176
+ task: { key: slackKey, parentTaskId: root.id },
177
+ });
178
+ });
179
+
180
+ test("a child with different Slack fields keeps its parent's custom key", async () => {
181
+ const root = await createTaskExtended("Custom root", { key: "shared/research/", ...slack });
182
+ const child = await createTaskExtended("Child", {
183
+ parentTaskId: root.id,
184
+ slackChannelId: "C_OTHER",
185
+ slackThreadTs: "123.456",
186
+ });
187
+ expect(child.key).toBe(root.key);
188
+ });
189
+
190
+ test("a child with chat context inherits its parent's default key", async () => {
191
+ const root = await createTaskExtended("Default root");
192
+ const child = await createTaskExtended("Child", {
193
+ parentTaskId: root.id,
194
+ contextKey: chatSdkContextKey("telegram:123:42"),
195
+ });
196
+ expect(child.key).toBe(root.key);
197
+ });
198
+
199
+ test("an explicit key wins for roots and children", async () => {
200
+ const root = await createTaskExtended("Custom root", { key: "shared/Custom/", ...slack });
201
+ const child = await createTaskExtended("Custom child", {
202
+ parentTaskId: root.id,
203
+ key: "shared/Other/",
204
+ });
205
+ expect(root.key).toBe("shared/custom/");
206
+ expect(child.key).toBe("shared/other/");
207
+ });
208
+
209
+ test("a Telegram chat-sdk root gets its source key", async () => {
210
+ const root = await createTaskExtended("Telegram root", {
211
+ source: "chat",
212
+ contextKey: chatSdkContextKey("telegram:-1001234567890:42"),
213
+ });
214
+ expect(root.key).toBe("shared/telegram/-1001234567890/thread:42/");
215
+ });
216
+
217
+ test("unsupported and invalid source keys retain the per-task default", async () => {
218
+ for (const options of [
219
+ {},
220
+ { contextKey: chatSdkContextKey("teams:conversation:thread") },
221
+ { ...slack, slackChannelId: "x".repeat(255) },
222
+ { ...slack, parentTaskId: crypto.randomUUID() },
223
+ ]) {
224
+ const root = await createTaskExtended("Default key", options);
225
+ expect(root.key).toBe(defaultAssetKey("task", root.id));
226
+ }
227
+ });
228
+
229
+ test("legacy createTask derives Slack keys and retains the default otherwise", async () => {
230
+ const agent = await createAgent({ name: "Source key agent", status: "idle", capabilities: [] });
231
+ const root = await createTask(agent.id, "Legacy Slack root", { source: "slack", ...slack });
232
+ const ordinary = await createTask(agent.id, "Legacy ordinary root");
233
+ expect(root.key).toBe(slackKey);
234
+ expect(ordinary.key).toBe(defaultAssetKey("task", ordinary.id));
235
+ });
236
+ });