@desplega.ai/agent-swarm 2.4.5 → 2.5.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 (272) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
  4. package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
  35. package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
  37. package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
  38. package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
  44. package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  49. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  50. package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
  51. package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
  52. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  53. package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
  54. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  55. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  56. package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
  57. package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
  58. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  59. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  60. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  61. package/dist/cli-pkaqk57w.js +128 -0
  62. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  63. package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
  64. package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
  65. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  66. package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
  67. package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
  68. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  69. package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
  70. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  71. package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
  72. package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
  73. package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
  74. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  75. package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
  76. package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
  77. package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
  78. package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
  79. package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
  80. package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
  81. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  82. package/dist/cli.js +29 -27
  83. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  84. package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
  85. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  86. package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
  87. package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
  88. package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
  89. package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
  90. package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
  91. package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
  92. package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
  93. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  94. package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
  95. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  96. package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
  97. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  98. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  99. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  100. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  101. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  102. package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
  103. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  104. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  105. package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
  106. package/dist/handlers-c9ajzhbn.js +41 -0
  107. package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
  108. package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
  109. package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
  110. package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
  111. package/dist/index-07ef7nkm.js +42 -0
  112. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  113. package/dist/index-7yts8n9s.js +43 -0
  114. package/dist/index-q3t40ryv.js +46 -0
  115. package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
  116. package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
  117. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  118. package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
  119. package/dist/lead-aayq64jn.js +79 -0
  120. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  121. package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
  122. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  123. package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  125. package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
  126. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  127. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  128. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  129. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  130. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  131. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  132. package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
  133. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  134. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  135. package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
  136. package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
  137. package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
  138. package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
  139. package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
  140. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  141. package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
  142. package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
  143. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  144. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  145. package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
  146. package/dist/worker-nwb789mg.js +79 -0
  147. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  148. package/openapi.json +785 -5
  149. package/package.json +9 -9
  150. package/src/assets/key.ts +17 -0
  151. package/src/azure-devops/handlers.ts +1 -1
  152. package/src/be/db/approvals.ts +18 -0
  153. package/src/be/db/session-moves.ts +164 -0
  154. package/src/be/db/session-tree.ts +384 -0
  155. package/src/be/db/tasks/read.ts +40 -4
  156. package/src/be/db/workflow-runs.ts +96 -0
  157. package/src/be/db.ts +80 -236
  158. package/src/be/onboarding.ts +3 -0
  159. package/src/be/skill-sync.ts +43 -2
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/bitbucket/api.ts +81 -0
  162. package/src/bitbucket/auth.ts +64 -0
  163. package/src/bitbucket/handlers.ts +434 -0
  164. package/src/bitbucket/index.ts +28 -0
  165. package/src/bitbucket/templates.ts +134 -0
  166. package/src/bitbucket/types.ts +65 -0
  167. package/src/commands/runner.ts +55 -3
  168. package/src/e2b/env.ts +1 -0
  169. package/src/extensions/contract-types.generated.ts +1 -1
  170. package/src/extensions/contract.ts +2 -2
  171. package/src/extensions/post-bridge.ts +1 -0
  172. package/src/github/handlers.ts +6 -6
  173. package/src/gitlab/handlers.ts +5 -5
  174. package/src/http/assets.ts +14 -2
  175. package/src/http/core.ts +12 -0
  176. package/src/http/index.ts +4 -0
  177. package/src/http/onboarding.ts +1 -0
  178. package/src/http/sessions.ts +366 -12
  179. package/src/http/skills.ts +48 -1
  180. package/src/http/tasks.ts +1 -1
  181. package/src/http/users.ts +1 -0
  182. package/src/http/webhooks.ts +141 -0
  183. package/src/integrations/chat-sdk/adapters.ts +19 -3
  184. package/src/integrations/chat-sdk/facts.ts +30 -12
  185. package/src/integrations/chat-sdk/outbound.ts +38 -1
  186. package/src/providers/acp-targets.ts +1 -1
  187. package/src/providers/amp-adapter.ts +1 -1
  188. package/src/providers/pi-mono-adapter.ts +5 -53
  189. package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
  190. package/src/slack/handlers.test.ts +158 -0
  191. package/src/slack/handlers.ts +19 -28
  192. package/src/tasks/context-key.ts +61 -1
  193. package/src/tests/bitbucket-auth.test.ts +156 -0
  194. package/src/tests/bitbucket-handlers.test.ts +425 -0
  195. package/src/tests/bitbucket-webhook.test.ts +220 -0
  196. package/src/tests/chat-sdk-inbound.test.ts +39 -0
  197. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  198. package/src/tests/context-key.test.ts +50 -0
  199. package/src/tests/core-auth.test.ts +5 -0
  200. package/src/tests/db-task-read-contract.test.ts +7 -0
  201. package/src/tests/db-task-write-contract.test.ts +6 -0
  202. package/src/tests/docs-index-footprint.test.js +3 -3
  203. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  204. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  205. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  206. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  207. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  208. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  209. package/src/tests/generate-mcp-docs.test.ts +101 -0
  210. package/src/tests/gitlab-handlers.test.ts +3 -3
  211. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  212. package/src/tests/integration-disable-flags.test.ts +36 -0
  213. package/src/tests/mcp-tools.test.ts +1 -1
  214. package/src/tests/onboarding.test.ts +12 -0
  215. package/src/tests/pi-codemode.test.ts +8 -2
  216. package/src/tests/preload-env.test.ts +57 -0
  217. package/src/tests/preload.ts +21 -0
  218. package/src/tests/prompt-template-remaining.test.ts +38 -0
  219. package/src/tests/run-root-coverage.test.ts +39 -0
  220. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  221. package/src/tests/runner-skills-refresh.test.ts +58 -0
  222. package/src/tests/secret-scrubber.test.ts +15 -0
  223. package/src/tests/sessions-move.test.ts +576 -0
  224. package/src/tests/sessions-tree.test.ts +493 -0
  225. package/src/tests/skill-files-http.test.ts +50 -0
  226. package/src/tests/skills-bundle.test.ts +99 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +2 -1
  228. package/src/tests/vcs-bitbucket.test.ts +91 -0
  229. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  230. package/src/tests/vcs-provider.test.ts +16 -1
  231. package/src/tests/vcs-tracking.test.ts +3 -3
  232. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  233. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  234. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  235. package/src/tests/workflow-foreach.test.ts +44 -23
  236. package/src/tests/workflow-step-claims.test.ts +6 -6
  237. package/src/tests/workflow-tla-races.test.ts +2 -2
  238. package/src/tools/script-delete.ts +1 -1
  239. package/src/tools/workflows/create-workflow.ts +5 -3
  240. package/src/types.ts +3 -2
  241. package/src/utils/config-env-export.ts +1 -0
  242. package/src/utils/secret-scrubber.ts +4 -0
  243. package/src/utils/skills-refresh.ts +83 -43
  244. package/src/vcs/bitbucket.ts +58 -0
  245. package/src/vcs/index.ts +2 -0
  246. package/src/vcs/types.ts +2 -2
  247. package/src/workflows/approval-resolution.ts +4 -1
  248. package/src/workflows/checkpoint.ts +9 -1
  249. package/src/workflows/constants.ts +3 -0
  250. package/src/workflows/definition.ts +48 -17
  251. package/src/workflows/engine.ts +25 -15
  252. package/src/workflows/executors/foreach.ts +45 -99
  253. package/src/workflows/foreach-body.ts +146 -0
  254. package/src/workflows/foreach-driver.ts +552 -0
  255. package/src/workflows/foreach-join.ts +206 -7
  256. package/src/workflows/node-timeout.ts +21 -0
  257. package/src/workflows/recovery.ts +68 -3
  258. package/src/workflows/resume.ts +221 -43
  259. package/src/workflows/retry-poller.ts +39 -24
  260. package/src/workflows/step-input.ts +59 -0
  261. package/src/workflows/task-step-routing.ts +114 -56
  262. package/templates/skills/slack-interaction/content.md +5 -0
  263. package/templates/skills/work-on-task/SKILL.md +1 -1
  264. package/templates/skills/work-on-task/content.md +1 -1
  265. package/dist/cli-9hstj09h.js +0 -2657
  266. package/dist/cli-yw91rnmw.js +0 -13
  267. package/dist/handlers-7gw64dxw.js +0 -39
  268. package/dist/index-bgdyvcnv.js +0 -40
  269. package/dist/index-gabbcdn2.js +0 -43
  270. package/dist/index-tfb26zhy.js +0 -41
  271. package/dist/lead-7e02z839.js +0 -77
  272. package/dist/worker-5snytpbd.js +0 -77
@@ -0,0 +1,576 @@
1
+ import { afterAll, beforeAll, expect, spyOn, test } from "bun:test";
2
+ import { createServer } from "node:http";
3
+ import * as audit from "../be/asset-key-audit";
4
+ import {
5
+ closeDb,
6
+ createAgent,
7
+ createTaskExtended,
8
+ createUser,
9
+ getDbClient,
10
+ getTaskById,
11
+ initDb,
12
+ insertTaskAttachment,
13
+ listRecentSessions,
14
+ moveTaskChainKeyInTx,
15
+ upsertInboxState,
16
+ } from "../be/db";
17
+ import * as sessionMoves from "../be/db/session-moves";
18
+ import { setSessionShares } from "../be/session-shares";
19
+ import * as assets from "../http/assets";
20
+ import { handleAssets } from "../http/assets";
21
+ import { SESSION_READ_DENIED } from "../http/session-access";
22
+ import { handleSessions } from "../http/sessions";
23
+ import { getPathSegments, parseQueryParams } from "../http/utils";
24
+ import { setRequestAuth } from "../utils/request-auth-context";
25
+ import { listenOnFreePort } from "./test-net";
26
+
27
+ let baseUrl: string;
28
+ let owner: string;
29
+ let other: string;
30
+ let agentId: string;
31
+ const server = createServer(async (req, res) => {
32
+ try {
33
+ const userId = req.headers["x-test-user"] as string | undefined;
34
+ const agent = req.headers["x-agent-id"] as string | undefined;
35
+ setRequestAuth(
36
+ req,
37
+ userId
38
+ ? { kind: "user", userId }
39
+ : agent
40
+ ? {
41
+ kind: "agent",
42
+ agentId: agent,
43
+ tokenKind: "agent",
44
+ taskId: null,
45
+ runtimeId: null,
46
+ tokenId: "test-agent",
47
+ expiresAt: "2099-01-01T00:00:00.000Z",
48
+ }
49
+ : { kind: "operator", fingerprint: "moves" },
50
+ );
51
+ const segments = getPathSegments(req.url ?? "");
52
+ const query = parseQueryParams(req.url ?? "");
53
+ if (await handleSessions(req, res, segments, query, agent)) return;
54
+ if (await handleAssets(req, res, segments, query, agent)) return;
55
+ res.writeHead(404).end();
56
+ } catch (error) {
57
+ res.writeHead(500).end(String(error));
58
+ }
59
+ });
60
+
61
+ beforeAll(async () => {
62
+ initDb(":memory:");
63
+ owner = (await createUser({ name: "Owner" })).id;
64
+ other = (await createUser({ name: "Other" })).id;
65
+ agentId = (await createAgent({ name: "Worker", isLead: false, status: "idle" })).id;
66
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
67
+ });
68
+
69
+ afterAll(async () => {
70
+ server.closeAllConnections();
71
+ await new Promise<void>((resolve) => server.close(() => resolve()));
72
+ closeDb();
73
+ });
74
+
75
+ async function call(path: string, body: unknown, userId: string | null = owner, agent?: string) {
76
+ const response = await fetch(`${baseUrl}/api/${path}`, {
77
+ method: path.startsWith("assets/") ? "PATCH" : "POST",
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ ...(userId ? { "X-Test-User": userId } : {}),
81
+ ...(agent ? { "X-Agent-ID": agent } : {}),
82
+ },
83
+ body: JSON.stringify(body),
84
+ });
85
+ return { status: response.status, body: await response.json() };
86
+ }
87
+
88
+ async function session(key = "shared/original/", userId = owner) {
89
+ const root = await createTaskExtended("root", { key, requestedByUserId: userId, agentId });
90
+ const child = await createTaskExtended("child", { parentTaskId: root.id, agentId });
91
+ const grandchild = await createTaskExtended("grandchild", { parentTaskId: child.id, agentId });
92
+ return { root, child, grandchild };
93
+ }
94
+
95
+ async function share(root: { id: string; requestedByUserId?: string }, access: "read" | "write") {
96
+ await setSessionShares(
97
+ { id: root.id, requestedByUserId: root.requestedByUserId ?? null },
98
+ { everyone: "none", users: [{ userId: owner, access }] },
99
+ other,
100
+ );
101
+ }
102
+
103
+ test("bulk moves every chain task and attachment mapping with history and preserves read state", async () => {
104
+ const { root, child, grandchild } = await session();
105
+ const attachment = await insertTaskAttachment({
106
+ taskId: child.id,
107
+ kind: "agent-fs",
108
+ name: "notes",
109
+ path: `moves/${child.id}`,
110
+ providerId: "agent-fs",
111
+ providerKey: `moves/${child.id}`,
112
+ });
113
+ await upsertInboxState({
114
+ userId: owner,
115
+ itemType: "to_read",
116
+ itemId: root.id,
117
+ status: "open",
118
+ lastReadAt: grandchild.lastUpdatedAt,
119
+ });
120
+ const before = await listRecentSessions({ viewer: { userId: owner, scope: "all" } });
121
+ const auditSpy = spyOn(audit, "auditAssetKeys");
122
+ try {
123
+ const result = await call("session-moves", {
124
+ rootTaskIds: [root.id, root.id],
125
+ groupKey: "shared/moved/",
126
+ });
127
+ expect(result).toEqual({ status: 200, body: { moved: [root.id], skipped: [] } });
128
+ expect(auditSpy).toHaveBeenCalledTimes(1);
129
+ } finally {
130
+ auditSpy.mockRestore();
131
+ }
132
+ for (const task of [root, child, grandchild]) {
133
+ const updated = await getTaskById(task.id);
134
+ expect(updated?.key).toBe("shared/moved/");
135
+ expect(updated?.lastUpdatedAt).toBe(task.lastUpdatedAt);
136
+ }
137
+ expect(
138
+ await getDbClient().query(
139
+ "SELECT entity_id FROM asset_key_history WHERE entity_type = 'task' AND entity_id IN (?, ?, ?)",
140
+ [root.id, child.id, grandchild.id],
141
+ ),
142
+ ).toHaveLength(3);
143
+ const mapping = await getDbClient().get<{ id: string; key: string }>(
144
+ "SELECT id, key FROM asset_key_mappings WHERE source_entity_id = ?",
145
+ [attachment.id],
146
+ );
147
+ expect(mapping?.key).toBe("shared/moved/");
148
+ expect(
149
+ await getDbClient().query(
150
+ "SELECT id FROM asset_key_history WHERE entity_type = 'file' AND entity_id = ? AND new_key = 'shared/moved/'",
151
+ [mapping!.id],
152
+ ),
153
+ ).toHaveLength(1);
154
+ const after = await listRecentSessions({ viewer: { userId: owner, scope: "all" } });
155
+ expect(after.find((s) => s.root.id === root.id)?.readState).toBe(
156
+ before.find((s) => s.root.id === root.id)?.readState,
157
+ );
158
+ expect(
159
+ (await call("session-moves", { rootTaskIds: [root.id], groupKey: "shared/moved/" })).body.moved,
160
+ ).toEqual([]);
161
+ expect(
162
+ (await call("session-moves", { rootTaskIds: [root.id], groupKey: null })).body.moved,
163
+ ).toEqual([root.id]);
164
+ for (const id of [root.id, child.id, grandchild.id])
165
+ expect((await getTaskById(id))?.key).toBe(`shared/task:${root.id}/`);
166
+ });
167
+
168
+ test("bulk skips missing, non-root, read-only and private roots while moving owned and write-shared roots", async () => {
169
+ const own = await session();
170
+ const read = await session(undefined, other);
171
+ const write = await session(undefined, other);
172
+ const hidden = await session(undefined, other);
173
+ await share(read.root, "read");
174
+ await share(write.root, "write");
175
+ const result = await call("session-moves", {
176
+ rootTaskIds: [
177
+ own.root.id,
178
+ read.root.id,
179
+ write.root.id,
180
+ hidden.root.id,
181
+ own.child.id,
182
+ "missing",
183
+ ],
184
+ groupKey: "shared/mixed/",
185
+ });
186
+ expect(result.status).toBe(200);
187
+ expect(result.body.moved).toEqual([own.root.id, write.root.id]);
188
+ expect(result.body.skipped.map((s: { rootTaskId: string }) => s.rootTaskId)).toEqual([
189
+ read.root.id,
190
+ hidden.root.id,
191
+ own.child.id,
192
+ "missing",
193
+ ]);
194
+ expect(result.body.skipped.every((s: { reason: string }) => s.reason.length > 0)).toBe(true);
195
+ expect((await getTaskById(read.root.id))?.key).toBe(read.root.key);
196
+ });
197
+
198
+ test("personal destination authorization rejects the request before any root moves", async () => {
199
+ const { root } = await session();
200
+ for (const body of [
201
+ { rootTaskIds: [root.id], groupKey: `personal/${other}/group/` },
202
+ { from: "shared/original/", to: `personal/${other}/group/` },
203
+ ]) {
204
+ expect((await call(`session-moves${"from" in body ? "/prefix" : ""}`, body)).status).toBe(403);
205
+ }
206
+ expect((await getTaskById(root.id))?.key).toBe(root.key);
207
+ expect(
208
+ (await call("session-moves", { rootTaskIds: [root.id], groupKey: `personal/${owner}/group/` }))
209
+ .status,
210
+ ).toBe(200);
211
+ });
212
+
213
+ test("a denied descendant prevents the entire chain move and does not count as remaining", async () => {
214
+ const denied = await session("shared/permission/");
215
+ const allowed = await session("shared/permission/");
216
+ const stranger = await createAgent({ name: "Stranger", isLead: false, status: "idle" });
217
+ await getDbClient().run("UPDATE agent_tasks SET agentId = ? WHERE id = ?", [
218
+ stranger.id,
219
+ denied.child.id,
220
+ ]);
221
+ const bulk = await call(
222
+ "session-moves",
223
+ {
224
+ rootTaskIds: [denied.root.id, allowed.root.id],
225
+ groupKey: "shared/allowed/",
226
+ },
227
+ null,
228
+ agentId,
229
+ );
230
+ expect(bulk.status).toBe(200);
231
+ expect(bulk.body.moved).toEqual([allowed.root.id]);
232
+ expect(bulk.body.skipped).toEqual([
233
+ { rootTaskId: denied.root.id, reason: "Not authorized to move this task namespace" },
234
+ ]);
235
+ const prefix = await call(
236
+ "session-moves/prefix",
237
+ {
238
+ from: "shared/permission/",
239
+ to: "shared/allowed/",
240
+ },
241
+ null,
242
+ agentId,
243
+ );
244
+ expect(prefix.status).toBe(200);
245
+ expect(prefix.body.moved).toEqual([]);
246
+ expect(prefix.body.remaining).toBe(0);
247
+ expect(prefix.body.skipped).toHaveLength(1);
248
+ expect((await getTaskById(denied.root.id))?.key).toBe(denied.root.key);
249
+ });
250
+
251
+ test("prefix rename preserves subgroups and skips shared sessions without exposing private roots", async () => {
252
+ const own = await session("shared/rename/b/");
253
+ const shared = await session("shared/rename/b/", other);
254
+ const hidden = await session("shared/rename/b/", other);
255
+ await share(shared.root, "write");
256
+ const result = await call("session-moves/prefix", {
257
+ from: "shared/rename/",
258
+ to: "shared/renamed/",
259
+ });
260
+ expect(result.status).toBe(200);
261
+ expect(result.body.moved).toEqual([own.root.id]);
262
+ expect(result.body.skipped).toEqual([{ rootTaskId: shared.root.id, reason: expect.any(String) }]);
263
+ expect(result.body.remaining).toBe(0);
264
+ expect((await getTaskById(own.grandchild.id))?.key).toBe("shared/renamed/b/");
265
+ expect((await getTaskById(hidden.root.id))?.key).toBe(hidden.root.key);
266
+ expect(
267
+ (await call("session-moves/prefix", { from: "shared/rename/", to: "shared/renamed/" })).body
268
+ .moved,
269
+ ).toEqual([]);
270
+ expect(
271
+ (await call("session-moves/prefix", { from: "shared/renamed/", to: null })).body.moved,
272
+ ).toEqual([own.root.id]);
273
+ expect((await getTaskById(own.root.id))?.key).toBe(`shared/task:${own.root.id}/`);
274
+ });
275
+
276
+ test("user prefix moves exclude ownerless roots and report only visible roots owned by others", async () => {
277
+ for (const from of ["shared/requester/", `personal/${other}/requester/`]) {
278
+ const own = await session(`${from}nested/`);
279
+ const ownerless = await createTaskExtended("ownerless", { key: `${from}nested/` });
280
+ const shared = await session(`${from}nested/`, other);
281
+ const privateRoot = await session(`${from}nested/`, other);
282
+ await share(shared.root, "write");
283
+ const checked: string[] = [];
284
+ const original = assets.canMutateTaskNamespace;
285
+ const checks = spyOn(assets, "canMutateTaskNamespace").mockImplementation(async (...args) => {
286
+ checked.push(args[0].id);
287
+ return original(...args);
288
+ });
289
+ try {
290
+ const result = await call("session-moves/prefix", { from, to: "shared/requester-moved/" });
291
+ expect(result).toEqual({
292
+ status: 200,
293
+ body: {
294
+ moved: [own.root.id],
295
+ remaining: 0,
296
+ skipped: [
297
+ { rootTaskId: shared.root.id, reason: "Only the session owner can move this group" },
298
+ ],
299
+ },
300
+ });
301
+ expect(checked.sort()).toEqual([own.root.id, own.child.id, own.grandchild.id].sort());
302
+ expect((await getTaskById(ownerless.id))?.key).toBe(ownerless.key);
303
+ expect((await getTaskById(privateRoot.root.id))?.key).toBe(privateRoot.root.key);
304
+ const deleted = await call("session-moves/prefix", { from, to: null });
305
+ expect(deleted.body.moved).toEqual([]);
306
+ expect(deleted.body.skipped).toHaveLength(1);
307
+ expect((await getTaskById(ownerless.id))?.key).toBe(ownerless.key);
308
+ } finally {
309
+ checks.mockRestore();
310
+ }
311
+ const bypass = await call("session-moves/prefix", { from, to: null }, null);
312
+ expect(bypass.status).toBe(200);
313
+ expect(bypass.body.moved.sort()).toEqual(
314
+ [ownerless.id, shared.root.id, privateRoot.root.id].sort(),
315
+ );
316
+ expect(bypass.body.skipped).toEqual([]);
317
+ }
318
+ });
319
+
320
+ test("owned roots denied task mutation are skipped without remaining work", async () => {
321
+ const { root } = await session("shared/owned-denied/");
322
+ const checks = spyOn(assets, "canMutateTaskNamespace").mockResolvedValue(false);
323
+ try {
324
+ const result = await call("session-moves/prefix", {
325
+ from: root.key,
326
+ to: "shared/owned-allowed/",
327
+ });
328
+ expect(result).toEqual({
329
+ status: 200,
330
+ body: {
331
+ moved: [],
332
+ remaining: 0,
333
+ skipped: [{ rootTaskId: root.id, reason: "Not authorized to move this task namespace" }],
334
+ },
335
+ });
336
+ } finally {
337
+ checks.mockRestore();
338
+ }
339
+ });
340
+
341
+ test("prefix skips replacement keys that remain in the source group", async () => {
342
+ const { root } = await session("shared/p/q/q/");
343
+ const move = { from: "shared/p/q/", to: "shared/p/" };
344
+ for (let i = 0; i < 2; i++) {
345
+ expect(await call("session-moves/prefix", move)).toEqual({
346
+ status: 200,
347
+ body: {
348
+ moved: [],
349
+ remaining: 0,
350
+ skipped: [{ rootTaskId: root.id, reason: "The new key stays inside the source group" }],
351
+ },
352
+ });
353
+ expect((await getTaskById(root.id))?.key).toBe(root.key);
354
+ }
355
+ });
356
+
357
+ test("bulk cannot distinguish missing roots from inaccessible roots or children", async () => {
358
+ const hidden = await session(undefined, other);
359
+ const own = await session();
360
+ const result = await call("session-moves", {
361
+ rootTaskIds: ["absent", hidden.root.id, hidden.child.id, own.child.id],
362
+ groupKey: null,
363
+ });
364
+ expect(result.status).toBe(200);
365
+ const reasons = result.body.skipped.map((row: { reason: string }) => row.reason);
366
+ expect(reasons[0]).toBe("Session not found or not accessible");
367
+ expect(reasons.slice(0, 3)).toEqual(Array(3).fill(reasons[0]));
368
+ expect(reasons[3]).toBe("Task is not a session root");
369
+ });
370
+
371
+ test("unexpected move errors propagate as 500 even when their message contains blocked until", async () => {
372
+ const { root } = await session("shared/unexpected/");
373
+ const failure = spyOn(audit, "auditAssetKeys").mockImplementation(() => {
374
+ throw new TypeError("unexpected blocked until failure");
375
+ });
376
+ try {
377
+ for (const [path, body] of [
378
+ ["session-moves", { rootTaskIds: [root.id], groupKey: null }],
379
+ ["session-moves/prefix", { from: root.key, to: null }],
380
+ ] as const) {
381
+ const response = await fetch(`${baseUrl}/api/${path}`, {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json", "X-Test-User": owner },
384
+ body: JSON.stringify(body),
385
+ });
386
+ expect(response.status).toBe(500);
387
+ expect(await response.text()).toContain("TypeError: unexpected blocked until failure");
388
+ }
389
+ } finally {
390
+ failure.mockRestore();
391
+ }
392
+ });
393
+
394
+ test("task chain move requires an active transaction", async () => {
395
+ await expect(moveTaskChainKeyInTx("unknown", [], "shared/transaction/")).rejects.toThrow(
396
+ "moveTaskChainKeyInTx requires an active transaction",
397
+ );
398
+ });
399
+
400
+ test("unexpected prefix scan errors propagate as 500", async () => {
401
+ const scan = spyOn(sessionMoves, "listPrefixRoots").mockRejectedValue(new Error("SQLITE_BUSY"));
402
+ try {
403
+ const response = await fetch(`${baseUrl}/api/session-moves/prefix`, {
404
+ method: "POST",
405
+ headers: { "Content-Type": "application/json", "X-Test-User": owner },
406
+ body: JSON.stringify({ from: "shared/unexpected/", to: null }),
407
+ });
408
+ expect(response.status).toBe(500);
409
+ expect(await response.text()).toContain("SQLITE_BUSY");
410
+ } finally {
411
+ scan.mockRestore();
412
+ }
413
+ });
414
+
415
+ test("prefix rejects namespace roots and destinations inside the source", async () => {
416
+ for (const from of ["shared/", `personal/${owner}/`]) {
417
+ expect((await call("session-moves/prefix", { from, to: null })).status).toBe(400);
418
+ }
419
+ for (const to of ["shared/a/", "shared/a/b/"]) {
420
+ expect((await call("session-moves/prefix", { from: "shared/a/", to })).status).toBe(400);
421
+ }
422
+ expect((await call("session-moves", { rootTaskIds: [], groupKey: null })).status).toBe(400);
423
+ expect(
424
+ (await call("session-moves", { rootTaskIds: Array(201).fill("id"), groupKey: null })).status,
425
+ ).toBe(400);
426
+ });
427
+
428
+ test("prefix normalizes the source before validation and suffix replacement", async () => {
429
+ const { root } = await session("shared/canonical/b/");
430
+ for (const from of ["SHARED/CANONICAL", "shared/canonical/"]) {
431
+ expect(
432
+ (await call("session-moves/prefix", { from, to: "shared/canonical/nested/" })).status,
433
+ ).toBe(400);
434
+ }
435
+ const result = await call("session-moves/prefix", {
436
+ from: "SHARED/CANONICAL",
437
+ to: "shared/normalized/",
438
+ });
439
+ expect(result.status).toBe(200);
440
+ expect(result.body.moved).toEqual([root.id]);
441
+ expect((await getTaskById(root.id))?.key).toBe("shared/normalized/b/");
442
+ });
443
+
444
+ test("prefix skips overlong results and excludes them from remaining", async () => {
445
+ const { root } = await session(`shared/long/${"x".repeat(120)}/`);
446
+ const result = await call("session-moves/prefix", {
447
+ from: "shared/long/",
448
+ to: `shared/${"y".repeat(140)}/`,
449
+ });
450
+ expect(result).toEqual({
451
+ status: 200,
452
+ body: {
453
+ moved: [],
454
+ skipped: [{ rootTaskId: root.id, reason: "Asset key cannot exceed 255 characters" }],
455
+ remaining: 0,
456
+ },
457
+ });
458
+ });
459
+
460
+ test("prefix pages past skipped roots and reaches zero with skipped roots remaining", async () => {
461
+ const skipped: string[] = [];
462
+ await getDbClient().transaction(async () => {
463
+ for (let i = 0; i < 201; i++) {
464
+ const root = await createTaskExtended("shared", {
465
+ key: "shared/paging/",
466
+ requestedByUserId: other,
467
+ });
468
+ skipped.push(root.id);
469
+ await share(root, "write");
470
+ }
471
+ for (let i = 0; i < 201; i++)
472
+ await createTaskExtended("owned", { key: "shared/paging/", requestedByUserId: owner });
473
+ });
474
+ const first = await call("session-moves/prefix", { from: "shared/paging/", to: "shared/paged/" });
475
+ expect(first.status).toBe(200);
476
+ expect(first.body.moved).toHaveLength(200);
477
+ expect(first.body.skipped).toHaveLength(201);
478
+ expect(first.body.remaining).toBe(1);
479
+ const second = await call("session-moves/prefix", {
480
+ from: "shared/paging/",
481
+ to: "shared/paged/",
482
+ });
483
+ expect(second.body.moved).toHaveLength(1);
484
+ expect(second.body.remaining).toBe(0);
485
+ const repeated = await call("session-moves/prefix", {
486
+ from: "shared/paging/",
487
+ to: "shared/paged/",
488
+ });
489
+ expect(repeated.body.moved).toEqual([]);
490
+ expect(repeated.body.remaining).toBe(0);
491
+ expect(repeated.body.skipped).toHaveLength(skipped.length);
492
+ });
493
+
494
+ test("audit warnings block all moves with the existing message and one audit per request", async () => {
495
+ const first = await session("shared/audit/");
496
+ const second = await session("shared/audit/");
497
+ const originalAudit = audit.auditAssetKeys;
498
+ const auditSpy = spyOn(audit, "auditAssetKeys").mockImplementation((db) => ({
499
+ ...originalAudit(db),
500
+ ok: false,
501
+ warningCount: 1,
502
+ }));
503
+ const prefixScan = spyOn(sessionMoves, "listPrefixRoots");
504
+ const sharedScan = spyOn(sessionMoves, "listSharedPrefixRoots");
505
+ try {
506
+ for (const body of [
507
+ { rootTaskIds: [first.root.id, second.root.id], groupKey: "shared/blocked/" },
508
+ { from: "shared/audit/", to: "shared/blocked/" },
509
+ ]) {
510
+ auditSpy.mockClear();
511
+ const result = await call(`session-moves${"from" in body ? "/prefix" : ""}`, body);
512
+ expect(result.status).toBe(409);
513
+ expect(result.body.error).toBe(
514
+ "Asset namespace moves are blocked until structural errors, unknown personal users, or provider mapping drift are repaired.",
515
+ );
516
+ expect(auditSpy).toHaveBeenCalledTimes(1);
517
+ expect(prefixScan).not.toHaveBeenCalled();
518
+ expect(sharedScan).not.toHaveBeenCalled();
519
+ expect((await getTaskById(first.root.id))?.key).toBe(first.root.key);
520
+ }
521
+ } finally {
522
+ auditSpy.mockRestore();
523
+ prefixScan.mockRestore();
524
+ sharedScan.mockRestore();
525
+ }
526
+ });
527
+
528
+ test("old route gives the standard denial for inaccessible sessions and 404 for missing assets", async () => {
529
+ const { root, child } = await session(undefined, other);
530
+ for (const cascade of [false, true]) {
531
+ for (const id of [root.id, child.id]) {
532
+ expect(await call(`assets/task/${id}/key`, { key: "shared/hidden/", cascade })).toEqual({
533
+ status: 403,
534
+ body: { error: SESSION_READ_DENIED },
535
+ });
536
+ }
537
+ expect(await call("assets/task/unknown/key", { key: "shared/missing/", cascade })).toEqual({
538
+ status: 404,
539
+ body: { error: "Asset not found" },
540
+ });
541
+ }
542
+ });
543
+
544
+ test("old route denies read-only sharees with and without cascade and admits writers, owners, agents and bypass operators", async () => {
545
+ const { root, child } = await session(undefined, other);
546
+ await share(root, "read");
547
+ for (const cascade of [false, true]) {
548
+ for (const id of [root.id, child.id]) {
549
+ const result = await call(`assets/task/${id}/key`, { key: "shared/denied/", cascade });
550
+ expect(result).toEqual({
551
+ status: 403,
552
+ body: { error: "You can view this session but not move it." },
553
+ });
554
+ }
555
+ }
556
+ await share(root, "write");
557
+ expect(
558
+ (await call(`assets/task/${root.id}/key`, { key: "shared/writer/", cascade: true })).status,
559
+ ).toBe(200);
560
+ expect((await call(`assets/task/${root.id}/key`, { key: "shared/owner/" }, other)).status).toBe(
561
+ 200,
562
+ );
563
+ expect(
564
+ (
565
+ await call(
566
+ `assets/task/${root.id}/key`,
567
+ { key: "shared/agent/", cascade: true },
568
+ null,
569
+ agentId,
570
+ )
571
+ ).status,
572
+ ).toBe(200);
573
+ expect((await call(`assets/task/${root.id}/key`, { key: "shared/operator/" }, null)).status).toBe(
574
+ 200,
575
+ );
576
+ });