@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
@@ -297,7 +297,8 @@ export function rowToAgentTask(row: AgentTaskRow): AgentTask {
297
297
  slackReplySent: !!row.slackReplySent,
298
298
  slackProgressMessageTs: row.slackProgressMessageTs ?? undefined,
299
299
  slackTreeRootMessageTs: row.slackTreeRootMessageTs ?? undefined,
300
- vcsProvider: (row.vcsProvider as "github" | "gitlab" | "azure-devops" | null) ?? undefined,
300
+ vcsProvider:
301
+ (row.vcsProvider as "github" | "gitlab" | "azure-devops" | "bitbucket" | null) ?? undefined,
301
302
  vcsRepo: row.vcsRepo ?? undefined,
302
303
  vcsEventType: row.vcsEventType ?? undefined,
303
304
  vcsNumber: row.vcsNumber ?? undefined,
@@ -713,18 +714,53 @@ export async function getTasksByStatus(status: AgentTaskStatus): Promise<AgentTa
713
714
  * in `src/types.ts`. SQL strings can't import a TS const — if you add a
714
715
  * new terminal status, grep for `NOT IN ('completed'` across this file.
715
716
  */
716
- export async function findTaskByVcs(vcsRepo: string, vcsNumber: number): Promise<AgentTask | null> {
717
+ /**
718
+ * Most recent active task for a provider + repo + PR/issue number. The
719
+ * provider is required: `owner/repo` exists on GitHub, GitLab and Bitbucket
720
+ * alike, so an unscoped lookup lets one host's webhook act on another host's
721
+ * task. A NULL `vcsProvider` is a legacy GitHub row (pre-provider writers), so
722
+ * the GitHub lookup still matches it.
723
+ */
724
+ export async function findTaskByVcs(
725
+ vcsRepo: string,
726
+ vcsNumber: number,
727
+ vcsProvider: NonNullable<AgentTask["vcsProvider"]>,
728
+ ): Promise<AgentTask | null> {
729
+ const providerClause =
730
+ vcsProvider === "github" ? "(vcsProvider = ? OR vcsProvider IS NULL)" : "vcsProvider = ?";
717
731
  const row = await getDbClient().get<AgentTaskRow>(
718
732
  `SELECT * FROM agent_tasks
719
- WHERE vcsRepo = ? AND vcsNumber = ?
733
+ WHERE vcsRepo = ? AND vcsNumber = ? AND ${providerClause}
720
734
  AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
721
735
  ORDER BY createdAt DESC
722
736
  LIMIT 1`,
723
- [vcsRepo, vcsNumber],
737
+ [vcsRepo, vcsNumber, vcsProvider],
724
738
  );
725
739
  return row ? rowToAgentTask(row) : null;
726
740
  }
727
741
 
742
+ /**
743
+ * Every active task for a provider + repo + PR/issue number, newest first.
744
+ * Same scoping as `findTaskByVcs`; for callers that must settle all of them
745
+ * (a PR close leaves no task worth running).
746
+ */
747
+ export async function findActiveTasksByVcs(
748
+ vcsRepo: string,
749
+ vcsNumber: number,
750
+ vcsProvider: NonNullable<AgentTask["vcsProvider"]>,
751
+ ): Promise<AgentTask[]> {
752
+ const providerClause =
753
+ vcsProvider === "github" ? "(vcsProvider = ? OR vcsProvider IS NULL)" : "vcsProvider = ?";
754
+ const rows = await getDbClient().query<AgentTaskRow>(
755
+ `SELECT * FROM agent_tasks
756
+ WHERE vcsRepo = ? AND vcsNumber = ? AND ${providerClause}
757
+ AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
758
+ ORDER BY createdAt DESC`,
759
+ [vcsRepo, vcsNumber, vcsProvider],
760
+ );
761
+ return rows.map(rowToAgentTask);
762
+ }
763
+
728
764
  /** @deprecated Use findTaskByVcs instead */
729
765
  export const findTaskByGitHub = findTaskByVcs;
730
766
 
@@ -1,5 +1,6 @@
1
1
  import type { WorkflowRunStatus } from "../../types";
2
2
  import { scrubSecrets } from "../../utils/secret-scrubber";
3
+ import { scrubJsonValue } from "../scrub-json";
3
4
  import { getDbClient } from "./runtime";
4
5
 
5
6
  // Parent/child links between workflow runs (migration 203): a `sub-workflow`
@@ -115,3 +116,98 @@ function emitAfterCommit(childRunId: string, parentStepId: string): void {
115
116
  );
116
117
  });
117
118
  }
119
+
120
+ /**
121
+ * Insert a foreach parent's missing child step rows as `pending` (queued), all
122
+ * in one transaction, so a crash never leaves a partial child set and the join
123
+ * always sees every item. Children that already exist (any row for the child
124
+ * node id) are skipped inside the transaction, which makes a concurrent
125
+ * re-execution of the same parent insert nothing twice. Returns how many rows
126
+ * this call inserted.
127
+ */
128
+ export async function createForeachChildSteps(
129
+ runId: string,
130
+ parentNodeId: string,
131
+ rows: Array<{
132
+ id: string;
133
+ nodeId: string;
134
+ nodeType: string;
135
+ input: unknown;
136
+ idempotencyKey: string;
137
+ }>,
138
+ ): Promise<number> {
139
+ if (rows.length === 0) return 0;
140
+ const prefix = `${parentNodeId}#`;
141
+ return getDbClient().transaction(async (tx) => {
142
+ const existing = await tx.query<{ nodeId: string }>(
143
+ "SELECT DISTINCT nodeId FROM workflow_run_steps WHERE runId = ? AND substr(nodeId, 1, ?) = ?",
144
+ [runId, prefix.length, prefix],
145
+ );
146
+ const existingNodeIds = new Set(existing.map((row) => row.nodeId));
147
+ const now = new Date().toISOString();
148
+ let created = 0;
149
+ for (const row of rows) {
150
+ if (existingNodeIds.has(row.nodeId)) continue;
151
+ await tx.run(
152
+ `INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, status, startedAt, input, idempotencyKey)
153
+ VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)`,
154
+ [
155
+ row.id,
156
+ runId,
157
+ row.nodeId,
158
+ row.nodeType,
159
+ now,
160
+ scrubJsonValue(row.input),
161
+ row.idempotencyKey,
162
+ ],
163
+ );
164
+ existingNodeIds.add(row.nodeId);
165
+ created++;
166
+ }
167
+ return created;
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Insert a new `pending` attempt row for each foreach item being retried. Each
173
+ * row reuses its item's child node id and gets a fresh step id, so the body
174
+ * starts a new invocation (a new task, child run, approval or wait) while the
175
+ * earlier attempt rows stay as history. `attempt` is the number of rows the
176
+ * item already has, and the idempotency key `<runId>:<childNodeId>:<attempt>`
177
+ * is UNIQUE, so a duplicated retry of the same attempt inserts nothing.
178
+ * Call it inside the retry's transaction. Returns how many rows it inserted.
179
+ */
180
+ export async function createForeachAttemptSteps(
181
+ runId: string,
182
+ rows: Array<{ nodeId: string; nodeType: string; itemKey: string; index: number }>,
183
+ ): Promise<number> {
184
+ if (rows.length === 0) return 0;
185
+ return getDbClient().transaction(async (tx) => {
186
+ const now = new Date().toISOString();
187
+ let created = 0;
188
+ for (const row of rows) {
189
+ const count = await tx.get<{ n: number }>(
190
+ "SELECT COUNT(*) AS n FROM workflow_run_steps WHERE runId = ? AND nodeId = ?",
191
+ [runId, row.nodeId],
192
+ );
193
+ const attempt = count?.n ?? 0;
194
+ const inserted = await tx.get<{ id: string }>(
195
+ `INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, status, startedAt, input, idempotencyKey)
196
+ VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
197
+ ON CONFLICT(idempotencyKey) DO NOTHING
198
+ RETURNING id`,
199
+ [
200
+ crypto.randomUUID(),
201
+ runId,
202
+ row.nodeId,
203
+ row.nodeType,
204
+ now,
205
+ scrubJsonValue({ itemKey: row.itemKey, index: row.index, attempt }),
206
+ `${runId}:${row.nodeId}:${attempt}`,
207
+ ],
208
+ );
209
+ if (inserted) created++;
210
+ }
211
+ return created;
212
+ });
213
+ }
package/src/be/db.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import pkg from "../../package.json";
3
- import { defaultAssetKey, normalizeAssetKey } from "../assets/key";
3
+ import { defaultAssetKey, normalizeAssetKey, sessionGroupKey } from "../assets/key";
4
4
  import {
5
5
  generateDefaultClaudeMd,
6
6
  generateDefaultIdentityMd,
@@ -195,7 +195,19 @@ import {
195
195
  } from "./db/context-versions";
196
196
  import { clearAgentModelProfileOnOverrideChange } from "./db/model-profiles";
197
197
  import { getDb, getDbClient } from "./db/runtime";
198
- import { type SessionViewer, sessionViewerCondition } from "./db/session-visibility";
198
+ import {
199
+ assertAssetKeyMovesAllowed,
200
+ configureSessionMoveDependencies,
201
+ moveTaskChainKeyInTx,
202
+ } from "./db/session-moves";
203
+ import {
204
+ type SessionFilters,
205
+ type SessionReadState,
206
+ type SessionRowFields,
207
+ sessionCountQuery,
208
+ sessionPageFields,
209
+ sessionSummaryQuery,
210
+ } from "./db/session-tree";
199
211
  import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
200
212
  import {
201
213
  type AgentTaskRow,
@@ -269,6 +281,7 @@ export { recordKeySeatMismatch } from "./db/api-keys";
269
281
  export {
270
282
  type ApprovalRequestSummary,
271
283
  type ApprovalVote,
284
+ getPendingApprovalsForTasks,
272
285
  getPendingApprovalVoteState,
273
286
  listApprovalRequestSummaries,
274
287
  recordApprovalVotes,
@@ -281,6 +294,11 @@ export {
281
294
  getLatestContextVersion,
282
295
  } from "./db/context-versions";
283
296
  export { getScriptRunExecutionArgs } from "./db/script-runs";
297
+ export {
298
+ AssetKeyMovesBlockedError,
299
+ assertAssetKeyMovesAllowed,
300
+ moveTaskChainKeyInTx,
301
+ } from "./db/session-moves";
284
302
 
285
303
  configureAgentDependencies({
286
304
  createLogEntry: (entry) => createLogEntry(entry),
@@ -300,6 +318,7 @@ export {
300
318
  export {
301
319
  type ExistingTrackerContextWork,
302
320
  type ExistingTrackerContextWorkReason,
321
+ findActiveTasksByVcs,
303
322
  findCompletedTaskInThread,
304
323
  findExistingLinearTrackerContextWork,
305
324
  findRecentCancelledTaskInThread,
@@ -387,6 +406,11 @@ configureTaskWriteDependencies({
387
406
  cascadeFailDependents: (...args) => cascadeFailDependents(...args),
388
407
  });
389
408
 
409
+ configureSessionMoveDependencies({
410
+ auditAssetKeys: () => auditAssetKeys(getDb()),
411
+ insertAssetKeyHistory: (...args) => insertAssetKeyHistory(...args),
412
+ });
413
+
390
414
  configureTaskReadDependencies({
391
415
  checkDependencies: (taskId) => checkDependencies(taskId),
392
416
  assetKeyPrefixPattern: (input) => assetKeyPrefixPattern(input),
@@ -400,7 +424,7 @@ type TaskTelemetryContext = {
400
424
  };
401
425
 
402
426
  /** LIKE pattern matching strings that start with `prefix` literally (use with `ESCAPE '\\'`). */
403
- function literalPrefixPattern(prefix: string): string {
427
+ export function literalPrefixPattern(prefix: string): string {
404
428
  return `${prefix.replace(/[\\%_]/g, "\\$&")}%`;
405
429
  }
406
430
 
@@ -1060,7 +1084,7 @@ export async function setSlackMessageTracking(
1060
1084
  export async function updateTaskVcs(
1061
1085
  taskId: string,
1062
1086
  vcs: {
1063
- vcsProvider: "github" | "gitlab" | "azure-devops";
1087
+ vcsProvider: "github" | "gitlab" | "azure-devops" | "bitbucket";
1064
1088
  vcsRepo: string;
1065
1089
  vcsNumber: number;
1066
1090
  vcsUrl: string;
@@ -1742,17 +1766,7 @@ export async function moveAssetKey(input: {
1742
1766
  descendantTaskIds?: readonly string[];
1743
1767
  }): Promise<false | { movedTaskIds?: string[] }> {
1744
1768
  const key = normalizeAssetKey(input.key);
1745
- // auditAssetKeys stays sync (shared with the boot audit); run it inside a
1746
- // client transaction so this request-path read cannot observe another
1747
- // request's uncommitted writes on the shared connection.
1748
- const audit = await getDbClient().transaction(async () => auditAssetKeys(getDb()), {
1749
- readOnly: true,
1750
- });
1751
- if (!audit.ok) {
1752
- throw new Error(
1753
- "Asset namespace moves are blocked until structural errors, unknown personal users, or provider mapping drift are repaired.",
1754
- );
1755
- }
1769
+ await assertAssetKeyMovesAllowed();
1756
1770
  const previousKey = await currentAssetKey(input.entityType, input.id);
1757
1771
  if (!previousKey) return false;
1758
1772
  if (previousKey === key && input.entityType !== "task") return {};
@@ -1769,60 +1783,13 @@ export async function moveAssetKey(input: {
1769
1783
  return getDbClient().transaction(async (tx) => {
1770
1784
  switch (input.entityType) {
1771
1785
  case "task": {
1772
- const root = await getTaskById(input.id);
1773
- if (!root) return false;
1774
- const tasks = [root];
1775
- for (const id of input.descendantTaskIds ?? []) {
1776
- const task = id === root.id ? null : await getTaskById(id);
1777
- if (task) tasks.push(task);
1778
- }
1779
- const movedTaskIds: string[] = [];
1780
- for (const task of tasks) {
1781
- if (task.key === key) continue;
1782
- // A file shared with another task keeps the namespace of the task whose attachment
1783
- // sources its mapping. Mappings with no attachment source follow any attaching task.
1784
- const mappedFiles = await tx.query<{ id: string; key: string }>(
1785
- `SELECT DISTINCT m.id, m."key" AS key
1786
- FROM asset_key_mappings m
1787
- JOIN task_attachments a
1788
- ON m.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
1789
- AND m.provider_org_id = COALESCE(a.agent_fs_org_id, '')
1790
- AND m.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
1791
- AND m.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
1792
- WHERE a.task_id = ?
1793
- AND (m.source_entity_type IS NOT 'task-attachment'
1794
- OR m.source_entity_id IN (SELECT id FROM task_attachments WHERE task_id = ?))`,
1795
- [task.id, task.id],
1796
- );
1797
- // The namespace is metadata, and lastUpdatedAt tracks task activity, as in updateTaskTitle.
1798
- await tx.run('UPDATE agent_tasks SET "key" = ?, updated_by = ? WHERE id = ?', [
1799
- key,
1800
- input.changedBy ?? null,
1801
- task.id,
1802
- ]);
1803
- for (const mapping of mappedFiles) {
1804
- if (mapping.key === key) continue;
1805
- await tx.run(
1806
- 'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = ? WHERE id = ?',
1807
- [key, now, input.changedBy ?? null, mapping.id],
1808
- );
1809
- await insertAssetKeyHistory({
1810
- entityType: "file",
1811
- entityId: mapping.id,
1812
- previousKey: mapping.key,
1813
- newKey: key,
1814
- changedBy: input.changedBy,
1815
- });
1816
- }
1817
- await insertAssetKeyHistory({
1818
- entityType: "task",
1819
- entityId: task.id,
1820
- previousKey: task.key,
1821
- newKey: key,
1822
- changedBy: input.changedBy,
1823
- });
1824
- movedTaskIds.push(task.id);
1825
- }
1786
+ const movedTaskIds = await moveTaskChainKeyInTx(
1787
+ input.id,
1788
+ input.descendantTaskIds ?? [],
1789
+ key,
1790
+ input.changedBy,
1791
+ );
1792
+ if (movedTaskIds === false) return false;
1826
1793
  return { movedTaskIds };
1827
1794
  }
1828
1795
  case "workflow":
@@ -2605,7 +2572,7 @@ export async function createTaskExtended(
2605
2572
  }
2606
2573
 
2607
2574
  // VCS identity (GitHub / GitLab issue / PR / MR + webhook routing)
2608
- // Webhook handlers locate active work via `findTaskByVcs(repo, number)`,
2575
+ // Webhook handlers locate active work via `findTaskByVcs(repo, number, provider)`,
2609
2576
  // so a resume / follow-up child MUST carry the full VCS identity or
2610
2577
  // subsequent review/update events get dropped.
2611
2578
  if (parent.vcsProvider && !options.vcsProvider) {
@@ -12921,7 +12888,7 @@ export async function getRootTaskChain(rootTaskId: string): Promise<AgentTask[]>
12921
12888
  return rows.map(rowToAgentTask);
12922
12889
  }
12923
12890
 
12924
- export interface SessionListItem {
12891
+ export interface SessionListItem extends SessionRowFields {
12925
12892
  root: AgentTask;
12926
12893
  chainTaskCount: number;
12927
12894
  lastActivityAt: string;
@@ -12934,42 +12901,18 @@ export interface SessionListItem {
12934
12901
  * The session list only renders a brief of the root; the full root + chain are
12935
12902
  * on `GET /api/sessions/{rootTaskId}`.
12936
12903
  */
12937
- export interface SessionListItemSummary {
12904
+ export interface SessionListItemSummary extends SessionRowFields {
12938
12905
  root: AgentTaskSummary;
12939
12906
  chainTaskCount: number;
12940
12907
  lastActivityAt: string;
12941
12908
  latestStatus: AgentTaskStatus;
12942
12909
  }
12943
12910
 
12944
- /**
12945
- * List the most recent sessions ordered by chain-wide latest activity.
12946
- * A "session" here is any task with `parentTaskId IS NULL` — its descendants
12947
- * (children, grand-children, …) are summarized via the recursive CTE.
12948
- *
12949
- * Single-pass CTE: seeds with root tasks matching the filter, walks the full
12950
- * descendant tree once, then aggregates chainCount / lastActivityAt /
12951
- * latestStatus in two lightweight non-recursive CTEs — replacing the original
12952
- * pattern of 3 correlated subqueries each re-running the recursion per row.
12953
- */
12954
- interface ListRecentSessionsOpts {
12911
+ interface ListRecentSessionsOpts extends SessionFilters {
12955
12912
  limit?: number;
12956
12913
  offset?: number;
12957
- /** Filter to root tasks whose `source` is in this list. Empty/undefined → no source filter. */
12958
- source?: string[];
12959
- /** Case-insensitive substring match against `r.task`. */
12960
- q?: string;
12961
- /** When set, restrict to root tasks where `requestedByUserId` equals this value. NULL rows are excluded. */
12962
- requestedByUserId?: string;
12963
- /**
12964
- * When set, restrict to root tasks whose `contextKey` starts with this
12965
- * literal prefix (`%`, `_` and `\` are escaped). Powers the UI contextual
12966
- * session panel's per-page dropdown.
12967
- */
12968
- contextKeyPrefix?: string;
12969
- /** When true, return slim `SessionListItemSummary` rows (default: full). */
12914
+ /** Return a slim root task summary instead of the full task. */
12970
12915
  slim?: boolean;
12971
- /** Restrict to sessions this viewer may open (session sharing). */
12972
- viewer?: SessionViewer;
12973
12916
  }
12974
12917
 
12975
12918
  export async function listRecentSessions(
@@ -12979,149 +12922,50 @@ export async function listRecentSessions(
12979
12922
  opts: ListRecentSessionsOpts & { slim: true },
12980
12923
  ): Promise<SessionListItemSummary[]>;
12981
12924
  export async function listRecentSessions(
12982
- opts?: ListRecentSessionsOpts,
12925
+ opts: ListRecentSessionsOpts = {},
12983
12926
  ): Promise<SessionListItem[] | SessionListItemSummary[]> {
12984
- const limit = opts?.limit ?? 25;
12985
- const offset = opts?.offset ?? 0;
12986
- const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
12987
- const q = opts?.q?.trim();
12988
- const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
12989
-
12990
- const conditions: string[] = ["r.parentTaskId IS NULL"];
12991
- const params: (string | number | null)[] = [];
12992
-
12993
- if (sources.length > 0) {
12994
- conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
12995
- params.push(...sources);
12996
- }
12997
- if (q && q.length > 0) {
12998
- conditions.push("(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)");
12999
- const like = `%${q.toLowerCase()}%`;
13000
- params.push(like, like);
13001
- }
13002
- if (requestedByUserId) {
13003
- conditions.push("r.requestedByUserId = ?");
13004
- params.push(requestedByUserId);
13005
- }
13006
- if (opts?.contextKeyPrefix) {
13007
- conditions.push(`r.contextKey LIKE ? ESCAPE '\\'`);
13008
- params.push(literalPrefixPattern(opts.contextKeyPrefix));
13009
- }
13010
- if (opts?.viewer) {
13011
- const viewer = sessionViewerCondition(opts.viewer);
13012
- conditions.push(viewer.sql);
13013
- params.push(...viewer.params);
13014
- }
13015
- params.push(limit, offset);
13016
-
12927
+ const query = sessionSummaryQuery(opts);
13017
12928
  const rootRows = await getDbClient().query<
13018
- AgentTaskRow & { __chainCount: number; __lastActivityAt: string; __latestStatus: string }
12929
+ AgentTaskRow & {
12930
+ __chainCount: number;
12931
+ __lastActivityAt: string;
12932
+ __latestStatus: AgentTaskStatus;
12933
+ __readState: SessionReadState | null;
12934
+ __snoozeUntil: string | null;
12935
+ __needsYou: number;
12936
+ }
13019
12937
  >(
13020
- `WITH RECURSIVE chain(root_id, id, lastUpdatedAt, status) AS (
13021
- SELECT r.id, r.id, r.lastUpdatedAt, r.status
13022
- FROM agent_tasks r
13023
- WHERE ${conditions.join(" AND ")}
13024
- UNION ALL
13025
- SELECT c.root_id, t.id, t.lastUpdatedAt, t.status
13026
- FROM agent_tasks t
13027
- JOIN chain c ON t.parentTaskId = c.id
13028
- ),
13029
- agg AS (
13030
- SELECT
13031
- root_id,
13032
- COUNT(*) AS chainCount,
13033
- MAX(lastUpdatedAt) AS lastActivityAt
13034
- FROM chain
13035
- GROUP BY root_id
13036
- ),
13037
- latest_status AS (
13038
- SELECT c.root_id, c.status AS latestStatus
13039
- FROM chain c
13040
- JOIN agg a ON c.root_id = a.root_id AND c.lastUpdatedAt = a.lastActivityAt
13041
- GROUP BY c.root_id
13042
- )
13043
- SELECT
13044
- r.*,
13045
- a.chainCount AS __chainCount,
13046
- a.lastActivityAt AS __lastActivityAt,
13047
- COALESCE(ls.latestStatus, r.status) AS __latestStatus
13048
- FROM agent_tasks r
13049
- JOIN agg a ON a.root_id = r.id
13050
- LEFT JOIN latest_status ls ON ls.root_id = r.id
13051
- ORDER BY a.lastActivityAt DESC, r.createdAt DESC, r.id DESC
13052
- LIMIT ? OFFSET ?`,
13053
- params,
13054
- );
13055
-
13056
- if (opts?.slim) {
13057
- return rootRows.map((row): SessionListItemSummary => {
13058
- const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
13059
- return {
13060
- root: rowToAgentTaskSummary(taskRow as AgentTaskRow),
13061
- chainTaskCount: __chainCount,
13062
- lastActivityAt: __lastActivityAt ?? row.lastUpdatedAt,
13063
- latestStatus: (__latestStatus as AgentTaskStatus) ?? row.status,
13064
- };
13065
- });
13066
- }
13067
-
13068
- return rootRows.map((row): SessionListItem => {
13069
- const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
13070
- return {
13071
- root: rowToAgentTask(taskRow as AgentTaskRow),
13072
- chainTaskCount: __chainCount,
13073
- lastActivityAt: __lastActivityAt ?? row.lastUpdatedAt,
13074
- latestStatus: (__latestStatus as AgentTaskStatus) ?? row.status,
13075
- };
13076
- });
12938
+ `SELECT r.*, a.chainCount AS __chainCount, a.lastActivityAt AS __lastActivityAt,
12939
+ a.latestStatus AS __latestStatus, a.readState AS __readState,
12940
+ a.snoozeUntil AS __snoozeUntil, a.needsYou AS __needsYou
12941
+ FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id
12942
+ ORDER BY ${opts.triage === "later" ? "a.snoozeUntil ASC," : ""}
12943
+ a.lastActivityAt DESC, r.createdAt DESC, r.id DESC LIMIT ? OFFSET ?`,
12944
+ [...query.params, opts.limit ?? 25, opts.offset ?? 0],
12945
+ );
12946
+ const fields = await sessionPageFields(
12947
+ rootRows.map((row) => ({
12948
+ id: row.id,
12949
+ requestedByUserId: row.requestedByUserId ?? undefined,
12950
+ })),
12951
+ );
12952
+ return rootRows.map((row) => ({
12953
+ root: opts.slim ? rowToAgentTaskSummary(row) : rowToAgentTask(row),
12954
+ chainTaskCount: row.__chainCount,
12955
+ lastActivityAt: row.__lastActivityAt,
12956
+ latestStatus: row.__latestStatus,
12957
+ ...(row.__readState ? { readState: row.__readState } : {}),
12958
+ snoozeUntil: row.__snoozeUntil,
12959
+ needsYou: !!row.__needsYou,
12960
+ groupKey: sessionGroupKey(row.key),
12961
+ ...fields.get(row.id),
12962
+ }));
13077
12963
  }
13078
12964
 
13079
- /**
13080
- * Filter-aware count of sessions (root tasks) matching the same `source` / `q`
13081
- * / `requestedByUserId` filters as `listRecentSessions`. Powers a correct
13082
- * `total` in the `/api/sessions` pager — a session is a root task, so this is
13083
- * a plain count, no recursive chain walk needed.
13084
- */
13085
- export async function countSessions(
13086
- opts?: Pick<
13087
- ListRecentSessionsOpts,
13088
- "source" | "q" | "requestedByUserId" | "contextKeyPrefix" | "viewer"
13089
- >,
13090
- ): Promise<number> {
13091
- const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
13092
- const q = opts?.q?.trim();
13093
- const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
13094
-
13095
- const conditions: string[] = ["r.parentTaskId IS NULL"];
13096
- const params: (string | null)[] = [];
13097
-
13098
- if (sources.length > 0) {
13099
- conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
13100
- params.push(...sources);
13101
- }
13102
- if (q && q.length > 0) {
13103
- conditions.push("(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)");
13104
- const like = `%${q.toLowerCase()}%`;
13105
- params.push(like, like);
13106
- }
13107
- if (requestedByUserId) {
13108
- conditions.push("r.requestedByUserId = ?");
13109
- params.push(requestedByUserId);
13110
- }
13111
- if (opts?.contextKeyPrefix) {
13112
- conditions.push(`r.contextKey LIKE ? ESCAPE '\\'`);
13113
- params.push(literalPrefixPattern(opts.contextKeyPrefix));
13114
- }
13115
- if (opts?.viewer) {
13116
- const viewer = sessionViewerCondition(opts.viewer);
13117
- conditions.push(viewer.sql);
13118
- params.push(...viewer.params);
13119
- }
13120
-
13121
- const row = await getDbClient().get<{ count: number }>(
13122
- `SELECT COUNT(*) AS count FROM agent_tasks r WHERE ${conditions.join(" AND ")}`,
13123
- params,
13124
- );
12965
+ /** Count roots directly unless a filter needs chain activity or participants. */
12966
+ export async function countSessions(opts: SessionFilters = {}): Promise<number> {
12967
+ const query = sessionCountQuery(opts);
12968
+ const row = await getDbClient().get<{ count: number }>(query.sql, query.params);
13125
12969
  return row?.count ?? 0;
13126
12970
  }
13127
12971
 
@@ -58,6 +58,7 @@ export const OnboardingIntegrationMethodSchema = z.enum([
58
58
  "slack",
59
59
  "github",
60
60
  "gitlab",
61
+ "bitbucket",
61
62
  "linear_oauth",
62
63
  "jira_oauth",
63
64
  ]);
@@ -139,6 +140,7 @@ export const OnboardingSignalsSchema = z.object({
139
140
  slack: z.boolean(),
140
141
  github: z.boolean(),
141
142
  gitlab: z.boolean(),
143
+ bitbucket: z.boolean(),
142
144
  linear: z.boolean(),
143
145
  jira: z.boolean(),
144
146
  }),
@@ -480,6 +482,7 @@ function inferIntegrationMethod(
480
482
  if (signals.integrations.slack) return "slack";
481
483
  if (signals.integrations.github) return "github";
482
484
  if (signals.integrations.gitlab) return "gitlab";
485
+ if (signals.integrations.bitbucket) return "bitbucket";
483
486
  if (signals.integrations.linear) return "linear_oauth";
484
487
  if (signals.integrations.jira) return "jira_oauth";
485
488
  return null;
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { homedir } from "node:os";
13
+ import type { SkillFile, SkillWithInstallInfo } from "../types";
13
14
  import {
14
15
  type SkillFsEntry,
15
16
  type SkillHarnessTarget,
@@ -71,8 +72,7 @@ export interface SkillsSignature {
71
72
  * uninstall, toggle, or skill-update mutates at least one of them. Output is
72
73
  * deterministic and contains no timestamps beyond per-row mutation fields.
73
74
  */
74
- export async function computeAgentSkillsSignature(agentId: string): Promise<SkillsSignature> {
75
- const skills = await getAgentSkills(agentId);
75
+ export function computeSkillsSignature(skills: readonly SkillWithInstallInfo[]): SkillsSignature {
76
76
  const sorted = [...skills].sort((a, b) => a.id.localeCompare(b.id));
77
77
  const canonical = JSON.stringify(
78
78
  sorted.map((s) => [
@@ -89,3 +89,44 @@ export async function computeAgentSkillsSignature(agentId: string): Promise<Skil
89
89
  const hash = new Bun.CryptoHasher("sha256").update(canonical).digest("hex");
90
90
  return { hash, count: sorted.length };
91
91
  }
92
+
93
+ /**
94
+ * Fetch the agent's skills and sign them.
95
+ *
96
+ * Callers that already hold the rows (the bundle endpoint) must use
97
+ * `computeSkillsSignature` on those exact rows instead of calling this: a
98
+ * second read can observe a newer edit and pair a fresh hash with stale
99
+ * payload bytes, which makes later signature probes skip the refresh forever.
100
+ */
101
+ export async function computeAgentSkillsSignature(agentId: string): Promise<SkillsSignature> {
102
+ return computeSkillsSignature(await getAgentSkills(agentId));
103
+ }
104
+
105
+ export interface SkillsBundle {
106
+ skills: (SkillWithInstallInfo & { files: SkillFile[] })[];
107
+ total: number;
108
+ signature: string;
109
+ }
110
+
111
+ /**
112
+ * Build the worker boot payload: every installed skill plus its bundled files,
113
+ * signed over the rows this same call returned.
114
+ *
115
+ * The readers are injected so the ordering the fix depends on is testable: the
116
+ * signature is taken before the first `readFiles`, so an edit that lands while
117
+ * the files are being read cannot pair stale bytes with a newer hash.
118
+ */
119
+ export async function buildSkillsBundle(
120
+ readSkills: () => Promise<SkillWithInstallInfo[]>,
121
+ readFiles: (skillId: string) => Promise<SkillFile[]>,
122
+ ): Promise<SkillsBundle> {
123
+ const skills = await readSkills();
124
+ const signature = computeSkillsSignature(skills).hash;
125
+ const entries = await Promise.all(
126
+ skills.map(async (skill) => ({
127
+ ...skill,
128
+ files: skill.isComplex ? await readFiles(skill.id) : [],
129
+ })),
130
+ );
131
+ return { skills: entries, total: entries.length, signature };
132
+ }