@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "2.4.5",
3
+ "version": "2.5.0",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -179,8 +179,8 @@
179
179
  "@desplega/model-catalog": "workspace:*",
180
180
  "@desplega/model-routing": "workspace:*",
181
181
  "@faker-js/faker": "^10.4.0",
182
- "@opencode/plugin": "2.1.2",
183
- "@opencode/sdk": "2.1.2",
182
+ "@opencode/plugin": "2.1.3",
183
+ "@opencode/sdk": "2.1.3",
184
184
  "@oxlint/plugins": "1.78.0",
185
185
  "@types/bun": "latest",
186
186
  "dependency-cruiser": "^18.0.0",
@@ -198,10 +198,10 @@
198
198
  "bun": ">=1.3.12"
199
199
  },
200
200
  "dependencies": {
201
- "@agentclientprotocol/sdk": "2.0.0",
201
+ "@agentclientprotocol/sdk": "2.0.1",
202
202
  "@ai-sdk/openai": "^3.0.41",
203
- "@anthropic-ai/claude-agent-sdk": "0.3.306",
204
- "@anthropic-ai/sdk": "^0.137.0",
203
+ "@anthropic-ai/claude-agent-sdk": "0.3.308",
204
+ "@anthropic-ai/sdk": "^1.1.0",
205
205
  "@asteasolutions/zod-to-openapi": "^8.0.0",
206
206
  "@aws-sdk/client-bedrock": "3.1048.0",
207
207
  "@chat-adapter/discord": "4.41.1",
@@ -211,9 +211,9 @@
211
211
  "@cursor/sdk": "1.0.40",
212
212
  "@desplega.ai/agent-fs-just-bash": "0.18.5",
213
213
  "@desplega.ai/business-use": "^0.4.2",
214
- "@earendil-works/pi-agent-core": "^1.3.0",
215
- "@earendil-works/pi-ai": "^1.3.0",
216
- "@earendil-works/pi-coding-agent": "^1.3.0",
214
+ "@earendil-works/pi-agent-core": "^1.4.0",
215
+ "@earendil-works/pi-ai": "^1.4.0",
216
+ "@earendil-works/pi-coding-agent": "^1.4.0",
217
217
  "@inkjs/ui": "^2.0.0",
218
218
  "@jitl/quickjs-singlefile-mjs-release-sync": "0.32.0",
219
219
  "@linear/sdk": "^77.0.0",
package/src/assets/key.ts CHANGED
@@ -83,6 +83,23 @@ export function defaultAssetKey(resource: AssetKeyResource, id: string): string
83
83
  return normalizeAssetKey(`shared/${resource}:${id}/`);
84
84
  }
85
85
 
86
+ /** Bounds for an indexed, literal prefix lookup on canonical asset keys. */
87
+ export function keyPrefixRange(prefix: string): { lo: string; hi: string } {
88
+ const lo = normalizeAssetKey(prefix);
89
+ return { lo, hi: `${lo.slice(0, -1)}${String.fromCharCode(lo.charCodeAt(lo.length - 1) + 1)}` };
90
+ }
91
+
92
+ export const SESSION_LEAF_KINDS = ["task", "thread", "schedule", "workflow"] as const;
93
+ const sessionLeafPattern = new RegExp(`/(?:${SESSION_LEAF_KINDS.join("|")}):[^/]+/$`);
94
+
95
+ /** Remove only a resource leaf, preserving user groups that contain a colon. */
96
+ export function sessionGroupKey(key: string): string {
97
+ const normalized = normalizeAssetKey(key);
98
+ const namespace = parseAssetKey(normalized);
99
+ if (namespace.relativeSegments.length === 0) return normalized;
100
+ return normalized.replace(sessionLeafPattern, "/");
101
+ }
102
+
86
103
  export function parseAssetKey(input: string): AssetNamespace {
87
104
  const key = normalizeAssetKey(input);
88
105
  const segments = key.slice(0, -1).split("/");
@@ -394,7 +394,7 @@ export async function handlePullRequestCommented(
394
394
  );
395
395
 
396
396
  // Check if there's already an active task for this PR
397
- const existingTask = await findTaskByVcs(repo, pr.pullRequestId);
397
+ const existingTask = await findTaskByVcs(repo, pr.pullRequestId, "azure-devops");
398
398
  const existingTaskNote = existingTask
399
399
  ? `\n\n_Note: There's an active task (${existingTask.id}) for this PR #${pr.pullRequestId}._`
400
400
  : "";
@@ -144,3 +144,21 @@ export function scrubApprovalVotesJson(approvals: ApprovalVote[]): ScrubbedText
144
144
  if (!breaksJsonValidity(raw, scrubbed)) return scrubbed;
145
145
  return JSON.stringify(scrubObject(approvals)) as ScrubbedText;
146
146
  }
147
+
148
+ /** Pending approval summaries for an already loaded session chain. */
149
+ export async function getPendingApprovalsForTasks(taskIds: string[]): Promise<
150
+ {
151
+ id: string;
152
+ status: "pending";
153
+ sourceTaskId: string;
154
+ title: string;
155
+ }[]
156
+ > {
157
+ if (!taskIds.length) return [];
158
+ return getDbClient().query(
159
+ `SELECT id, status, sourceTaskId, title FROM approval_requests
160
+ WHERE sourceTaskId IN (${taskIds.map(() => "?").join(",")}) AND status = 'pending'
161
+ ORDER BY createdAt, id`,
162
+ taskIds,
163
+ );
164
+ }
@@ -0,0 +1,164 @@
1
+ import { keyPrefixRange } from "../../assets/key";
2
+ import type { AssetEntityType } from "../../types";
3
+ import type { AssetKeyAuditResult } from "../asset-key-audit";
4
+ import { isInTransaction } from "../db-client";
5
+ import { getDbClient } from "./runtime";
6
+ import { sessionViewerCondition } from "./session-visibility";
7
+ import { getTaskById } from "./tasks/read";
8
+
9
+ type SessionMoveDependencies = {
10
+ /** The sync boot audit (`auditAssetKeys`) over the raw handle, which only db.ts may use. */
11
+ auditAssetKeys: () => AssetKeyAuditResult;
12
+ insertAssetKeyHistory: (input: {
13
+ entityType: AssetEntityType;
14
+ entityId: string;
15
+ previousKey?: string | null;
16
+ newKey: string;
17
+ changedBy?: string;
18
+ }) => Promise<void>;
19
+ };
20
+
21
+ let insertAssetKeyHistory: SessionMoveDependencies["insertAssetKeyHistory"];
22
+ let auditAssetKeys: SessionMoveDependencies["auditAssetKeys"];
23
+
24
+ export function configureSessionMoveDependencies(value: SessionMoveDependencies): void {
25
+ insertAssetKeyHistory = value.insertAssetKeyHistory;
26
+ auditAssetKeys = value.auditAssetKeys;
27
+ }
28
+
29
+ export class AssetKeyMovesBlockedError extends Error {
30
+ constructor() {
31
+ super(
32
+ "Asset namespace moves are blocked until structural errors, unknown personal users, or provider mapping drift are repaired.",
33
+ );
34
+ this.name = "AssetKeyMovesBlockedError";
35
+ }
36
+ }
37
+
38
+ export async function assertAssetKeyMovesAllowed(): Promise<void> {
39
+ // auditAssetKeys stays sync (shared with the boot audit); run it inside a
40
+ // client transaction so this request-path read cannot observe another
41
+ // request's uncommitted writes on the shared connection.
42
+ const audit = await getDbClient().transaction(async () => auditAssetKeys(), {
43
+ readOnly: true,
44
+ });
45
+ if (!audit.ok) {
46
+ throw new AssetKeyMovesBlockedError();
47
+ }
48
+ }
49
+
50
+ /** Move only authorized task ids. The caller owns the transaction and audit gate. */
51
+ export async function moveTaskChainKeyInTx(
52
+ rootId: string,
53
+ taskIds: readonly string[],
54
+ key: string,
55
+ changedBy?: string,
56
+ ): Promise<string[] | false> {
57
+ if (!isInTransaction()) {
58
+ throw new Error("moveTaskChainKeyInTx requires an active transaction");
59
+ }
60
+ const client = getDbClient();
61
+ const now = new Date().toISOString();
62
+ const root = await getTaskById(rootId);
63
+ if (!root) return false;
64
+ const tasks = [root];
65
+ for (const id of taskIds) {
66
+ const task = id === root.id ? null : await getTaskById(id);
67
+ if (task) tasks.push(task);
68
+ }
69
+ const movedTaskIds: string[] = [];
70
+ for (const task of tasks) {
71
+ if (task.key === key) continue;
72
+ // A file shared with another task keeps the namespace of the task whose attachment
73
+ // sources its mapping. Mappings with no attachment source follow any attaching task.
74
+ const mappedFiles = await client.query<{ id: string; key: string }>(
75
+ `SELECT DISTINCT m.id, m."key" AS key
76
+ FROM asset_key_mappings m
77
+ JOIN task_attachments a
78
+ ON m.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
79
+ AND m.provider_org_id = COALESCE(a.agent_fs_org_id, '')
80
+ AND m.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
81
+ AND m.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
82
+ WHERE a.task_id = ?
83
+ AND (m.source_entity_type IS NOT 'task-attachment'
84
+ OR m.source_entity_id IN (SELECT id FROM task_attachments WHERE task_id = ?))`,
85
+ [task.id, task.id],
86
+ );
87
+ // The namespace is metadata, and lastUpdatedAt tracks task activity, as in updateTaskTitle.
88
+ await client.run('UPDATE agent_tasks SET "key" = ?, updated_by = ? WHERE id = ?', [
89
+ key,
90
+ changedBy ?? null,
91
+ task.id,
92
+ ]);
93
+ for (const mapping of mappedFiles) {
94
+ if (mapping.key === key) continue;
95
+ await client.run(
96
+ 'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = ? WHERE id = ?',
97
+ [key, now, changedBy ?? null, mapping.id],
98
+ );
99
+ await insertAssetKeyHistory({
100
+ entityType: "file",
101
+ entityId: mapping.id,
102
+ previousKey: mapping.key,
103
+ newKey: key,
104
+ changedBy,
105
+ });
106
+ }
107
+ await insertAssetKeyHistory({
108
+ entityType: "task",
109
+ entityId: task.id,
110
+ previousKey: task.key,
111
+ newKey: key,
112
+ changedBy,
113
+ });
114
+ movedTaskIds.push(task.id);
115
+ }
116
+ return movedTaskIds;
117
+ }
118
+
119
+ export type SessionKeyMove = { rootId: string; taskIds: string[]; key: string };
120
+
121
+ /** The caller must complete the key audit before this operation. */
122
+ export async function moveSessionChains(input: {
123
+ moves: SessionKeyMove[];
124
+ changedBy?: string;
125
+ }): Promise<string[]> {
126
+ const moved: string[] = [];
127
+ for (const move of input.moves) {
128
+ const taskIds = await getDbClient().transaction(async () =>
129
+ moveTaskChainKeyInTx(move.rootId, move.taskIds, move.key, input.changedBy),
130
+ );
131
+ if (taskIds && taskIds.length > 0) moved.push(move.rootId);
132
+ }
133
+ return moved;
134
+ }
135
+
136
+ export async function listPrefixRoots(input: {
137
+ from: string;
138
+ userId?: string | null;
139
+ limit: number;
140
+ offset?: number;
141
+ }): Promise<{ id: string; key: string }[]> {
142
+ const { lo, hi } = keyPrefixRange(input.from);
143
+ return getDbClient().query<{ id: string; key: string }>(
144
+ `SELECT r.id, r."key" FROM agent_tasks r
145
+ WHERE r.parentTaskId IS NULL AND r."key" >= ? AND r."key" < ?
146
+ ${input.userId !== undefined ? "AND r.requestedByUserId = ?" : ""}
147
+ ORDER BY r.id LIMIT ? OFFSET ?`,
148
+ [lo, hi, ...(input.userId !== undefined ? [input.userId] : []), input.limit, input.offset ?? 0],
149
+ );
150
+ }
151
+
152
+ export async function listSharedPrefixRoots(input: {
153
+ from: string;
154
+ userId: string | null;
155
+ }): Promise<{ id: string }[]> {
156
+ const { lo, hi } = keyPrefixRange(input.from);
157
+ const visibility = sessionViewerCondition({ userId: input.userId, scope: "shared" });
158
+ return getDbClient().query<{ id: string }>(
159
+ `SELECT r.id FROM agent_tasks r
160
+ WHERE r.parentTaskId IS NULL AND r."key" >= ? AND r."key" < ?
161
+ AND ${visibility.sql} ORDER BY r.id`,
162
+ [lo, hi, ...visibility.params],
163
+ );
164
+ }
@@ -0,0 +1,384 @@
1
+ import {
2
+ keyPrefixRange,
3
+ normalizeAssetKey,
4
+ SESSION_LEAF_KINDS,
5
+ sessionGroupKey,
6
+ } from "../../assets/key";
7
+ import { isTerminalTaskStatus, TERMINAL_TASK_STATUSES } from "../../types";
8
+ import { getDbClient, literalPrefixPattern } from "../db";
9
+ import { type SessionViewer, sessionViewerCondition } from "./session-visibility";
10
+
11
+ export type SessionReadState = "unread" | "read" | "later" | "done";
12
+ export interface SessionFilters {
13
+ /** Restrict roots to these task sources. */
14
+ source?: string[];
15
+ /** Match root task text or title with a case-insensitive substring. */
16
+ q?: string;
17
+ /** Match the root requester, excluding NULL requester rows. */
18
+ requestedByUserId?: string;
19
+ /** Literal context prefix for the UI session panel, escaping %, _ and backslash. */
20
+ contextKeyPrefix?: string;
21
+ /** Apply this user's session ownership and share visibility. */
22
+ viewer?: SessionViewer;
23
+ /** Match roots under this literal canonical asset key prefix. */
24
+ keyPrefix?: string;
25
+ /** Match this exact group and its direct resource leaves. */
26
+ groupKey?: string;
27
+ /** Match shared and viewer-personal namespace roots and direct leaves. */
28
+ noGroup?: boolean;
29
+ /** Filter by chain-wide read state or the Inbox section. */
30
+ triage?: "inbox" | "later" | "done" | "unread";
31
+ /** Filter by latest chain status or pending human attention. */
32
+ status?: "running" | "failed" | "needs";
33
+ /** Match a requester anywhere in the chain or an active share recipient. */
34
+ withUserId?: string;
35
+ /** Use this ISO timestamp for read-state comparisons. */
36
+ now?: string;
37
+ /** Exclude ownerless roots for user viewers to match the tree's visibility. */
38
+ tree?: boolean;
39
+ /** Match exactly this root (one session's summary, e.g. its read state). */
40
+ rootId?: string;
41
+ }
42
+
43
+ type SqlFragment = { sql: string; params: (string | number | null)[] };
44
+
45
+ /** Shared read-state rule with bound ISO timestamps for the list and tree. */
46
+ function sessionReadStateSql(now = new Date().toISOString()): SqlFragment {
47
+ return {
48
+ sql: `CASE
49
+ WHEN s.id IS NULL THEN CASE WHEN a.lastActivityAt > ? THEN 'unread' ELSE 'read' END
50
+ WHEN s.status = 'dismissed' THEN 'done'
51
+ WHEN s.status = 'done' THEN CASE WHEN a.lastActivityAt > s.lastUpdatedAt THEN 'unread' ELSE 'done' END
52
+ WHEN s.status = 'snoozed' THEN CASE WHEN s.snoozeUntil > ? THEN 'later' ELSE 'unread' END
53
+ WHEN a.lastActivityAt > COALESCE(s.lastReadAt, s.lastUpdatedAt) THEN 'unread'
54
+ ELSE 'read'
55
+ END`,
56
+ params: [new Date(Date.parse(now) - 7 * 86400000).toISOString(), now],
57
+ };
58
+ }
59
+
60
+ function groupCondition(group: string): SqlFragment {
61
+ const { lo, hi } = keyPrefixRange(group);
62
+ // The remaining key must be one resource leaf, with no nested path.
63
+ // SQLite substr/length count code points, so count them here too (not UTF-16 units).
64
+ const start = [...lo].length + 1;
65
+ return {
66
+ sql: `(r."key" >= ? AND r."key" < ? AND (r."key" = ? OR (
67
+ instr(substr(r."key", ?, length(r."key") - ?), '/') = 0 AND
68
+ (${SESSION_LEAF_KINDS.map(() => 'substr(r."key", ?) GLOB ?').join(" OR ")})
69
+ )))`,
70
+ params: [
71
+ lo,
72
+ hi,
73
+ lo,
74
+ start,
75
+ start,
76
+ ...SESSION_LEAF_KINDS.flatMap((kind) => [start, `${kind}:?*/`]),
77
+ ],
78
+ };
79
+ }
80
+
81
+ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
82
+ const conditions = ["r.parentTaskId IS NULL"];
83
+ const params: SqlFragment["params"] = [];
84
+ const add = (fragment: SqlFragment) => {
85
+ conditions.push(fragment.sql);
86
+ params.push(...fragment.params);
87
+ };
88
+ if (opts.rootId) add({ sql: "r.id = ?", params: [opts.rootId] });
89
+ const sources = opts.source?.filter(Boolean) ?? [];
90
+ if (sources.length)
91
+ add({ sql: `r.source IN (${sources.map(() => "?").join(",")})`, params: sources });
92
+ if (opts.q?.trim()) {
93
+ const like = `%${opts.q.trim().toLowerCase()}%`;
94
+ add({
95
+ sql: "(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)",
96
+ params: [like, like],
97
+ });
98
+ }
99
+ if (opts.requestedByUserId?.trim())
100
+ add({ sql: "r.requestedByUserId = ?", params: [opts.requestedByUserId.trim()] });
101
+ if (opts.contextKeyPrefix)
102
+ add({
103
+ sql: "r.contextKey LIKE ? ESCAPE '\\'",
104
+ params: [literalPrefixPattern(opts.contextKeyPrefix)],
105
+ });
106
+ if (opts.viewer) {
107
+ add(sessionViewerCondition(opts.viewer));
108
+ if (opts.tree) conditions.push("r.requestedByUserId IS NOT NULL");
109
+ }
110
+ if (opts.keyPrefix) {
111
+ const { lo, hi } = keyPrefixRange(opts.keyPrefix);
112
+ add({ sql: 'r."key" >= ? AND r."key" < ?', params: [lo, hi] });
113
+ }
114
+ if (opts.groupKey) add(groupCondition(normalizeAssetKey(opts.groupKey)));
115
+ if (opts.noGroup) {
116
+ const groups = [groupCondition("shared/")];
117
+ if (opts.viewer?.userId) groups.push(groupCondition(`personal/${opts.viewer.userId}/`));
118
+ add({
119
+ sql: `(${groups.map((g) => g.sql).join(" OR ")})`,
120
+ params: groups.flatMap((g) => g.params),
121
+ });
122
+ }
123
+ if (opts.triage === "later" || opts.triage === "done") {
124
+ add({
125
+ sql: `r.id IN (SELECT itemId FROM inbox_item_state
126
+ WHERE userId = ? AND itemType = 'to_read' AND status IN (${opts.triage === "later" ? "'snoozed'" : "'done','dismissed'"}))`,
127
+ params: [opts.viewer?.userId ?? null],
128
+ });
129
+ }
130
+ return { sql: conditions.join(" AND "), params };
131
+ }
132
+
133
+ function excludesTriaged(opts: SessionFilters): boolean {
134
+ return !!(opts.viewer?.userId && (opts.groupKey || opts.keyPrefix || opts.noGroup));
135
+ }
136
+
137
+ function hasSessionChainFilter(opts: SessionFilters): boolean {
138
+ return !!(opts.triage || opts.status || opts.withUserId || excludesTriaged(opts));
139
+ }
140
+
141
+ const runningSql = `latestStatus NOT IN (${TERMINAL_TASK_STATUSES.map((s) => `'${s}'`).join(",")})`;
142
+
143
+ /**
144
+ * A session starts at a parentTaskId IS NULL root. One recursive CTE computes
145
+ * chain-wide latest activity for list ordering. Root filters precede recursion.
146
+ * Read state and chain filters follow aggregation.
147
+ */
148
+ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
149
+ const root = sessionRootFilter(opts);
150
+ const read = opts.viewer?.userId ? sessionReadStateSql(opts.now) : { sql: "NULL", params: [] };
151
+ const params = [...root.params];
152
+ if (opts.withUserId) params.push(opts.withUserId);
153
+ params.push(...read.params, opts.viewer?.userId ?? null);
154
+ const filters: string[] = [];
155
+ if (opts.triage === "unread") filters.push("readState = 'unread'");
156
+ else if (opts.triage === "later" || opts.triage === "done") {
157
+ filters.push("readState = ?");
158
+ params.push(opts.triage);
159
+ } else if (opts.triage === "inbox" || excludesTriaged(opts)) {
160
+ filters.push("(readState IS NULL OR readState IN ('read','unread'))");
161
+ }
162
+ if (opts.status === "running") filters.push(runningSql);
163
+ if (opts.status === "failed") filters.push("latestStatus = 'failed'");
164
+ if (opts.status === "needs") filters.push("needsYou = 1");
165
+ if (opts.withUserId) {
166
+ filters.push(`(a.withUser > 0 OR EXISTS (SELECT 1 FROM session_shares sh
167
+ WHERE sh.rootTaskId = a.root_id AND sh.revokedAt IS NULL AND sh.userId = ?))`);
168
+ params.push(opts.withUserId);
169
+ }
170
+ return {
171
+ // SQLite takes the bare status from the row with MAX(lastUpdatedAt).
172
+ // Keep this as the only MIN/MAX aggregate so status follows activity.
173
+ sql: `WITH RECURSIVE chain(root_id, id, lastUpdatedAt, status, requestedByUserId) AS (
174
+ SELECT r.id, r.id, r.lastUpdatedAt, r.status, r.requestedByUserId
175
+ FROM agent_tasks r WHERE ${root.sql}
176
+ UNION ALL
177
+ SELECT c.root_id, t.id, t.lastUpdatedAt, t.status, t.requestedByUserId
178
+ FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
179
+ ), agg AS (
180
+ SELECT root_id, COUNT(*) AS chainCount,
181
+ status AS latestStatus, MAX(lastUpdatedAt) AS lastActivityAt,
182
+ ${opts.withUserId ? "SUM(requestedByUserId = ?) AS withUser," : ""}
183
+ SUM(EXISTS (SELECT 1 FROM approval_requests ar
184
+ WHERE ar.sourceTaskId = chain.id AND ar.status = 'pending')) AS pendingApproval
185
+ FROM chain GROUP BY root_id
186
+ ), state AS (
187
+ SELECT a.*, ${read.sql} AS readState, s.snoozeUntil
188
+ FROM agg a LEFT JOIN inbox_item_state s
189
+ ON s.userId = ? AND s.itemType = 'to_read' AND s.itemId = a.root_id
190
+ ), summary AS (
191
+ SELECT *, (COALESCE(readState = 'unread' AND latestStatus = 'failed', 0) OR pendingApproval) AS needsYou
192
+ FROM state
193
+ )
194
+ SELECT a.* FROM summary a ${filters.length ? `WHERE ${filters.join(" AND ")}` : ""}`,
195
+ params,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * One session's read state for a user, with the same rule and chain activity
201
+ * as the list rows. Null when the user cannot see the root.
202
+ */
203
+ export async function getSessionReadState(
204
+ rootId: string,
205
+ userId: string,
206
+ now?: string,
207
+ ): Promise<{ readState: SessionReadState; lastActivityAt: string } | null> {
208
+ const query = sessionSummaryQuery({ rootId, viewer: { userId, scope: "all" }, now });
209
+ const row = await getDbClient().get<{ readState: SessionReadState; lastActivityAt: string }>(
210
+ `SELECT readState, lastActivityAt FROM (${query.sql})`,
211
+ query.params,
212
+ );
213
+ return row?.readState ? { readState: row.readState, lastActivityAt: row.lastActivityAt } : null;
214
+ }
215
+
216
+ /** Count roots directly unless a filter needs chain activity or participants. */
217
+ export function sessionCountQuery(opts: SessionFilters = {}): SqlFragment {
218
+ const chain = hasSessionChainFilter(opts);
219
+ const query = chain ? sessionSummaryQuery(opts) : sessionRootFilter(opts);
220
+ return {
221
+ sql: chain
222
+ ? `SELECT COUNT(*) AS count FROM (${query.sql})`
223
+ : `SELECT COUNT(*) AS count FROM agent_tasks r WHERE ${query.sql}`,
224
+ params: query.params,
225
+ };
226
+ }
227
+
228
+ export interface SessionRowFields {
229
+ readState?: SessionReadState;
230
+ snoozeUntil?: string | null;
231
+ needsYou?: boolean;
232
+ groupKey?: string;
233
+ shared?: boolean;
234
+ people?: { id: string; name: string }[];
235
+ peopleCount?: number;
236
+ preview?: string | null;
237
+ /** Active shares with their access. `userId` null means everyone on the swarm. */
238
+ shares?: { userId: string | null; name: string | null; access: "read" | "write" }[];
239
+ }
240
+
241
+ /** Two page-wide queries load people, shares and bounded output previews. */
242
+ export async function sessionPageFields(
243
+ roots: { id: string; requestedByUserId?: string }[],
244
+ ): Promise<Map<string, SessionRowFields>> {
245
+ const result = new Map<string, SessionRowFields>();
246
+ if (!roots.length) return result;
247
+ const ids = roots.map((r) => r.id);
248
+ const [tasks, shares] = await Promise.all([
249
+ getDbClient().query<{
250
+ root_id: string;
251
+ id: string | null;
252
+ name: string | null;
253
+ preview: string | null;
254
+ }>(
255
+ `WITH RECURSIVE chain(root_id, id) AS (
256
+ SELECT id, id FROM agent_tasks WHERE id IN (${ids.map(() => "?").join(",")})
257
+ UNION ALL SELECT c.root_id, t.id FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
258
+ ) SELECT c.root_id, t.requestedByUserId AS id, u.name,
259
+ CASE WHEN t.source = 'system' AND t.taskType = 'follow-up' THEN NULL ELSE NULLIF(trim(substr(t.output, 1, 200)), '') END AS preview
260
+ FROM chain c CROSS JOIN agent_tasks t ON t.id = c.id
261
+ LEFT JOIN users u ON u.id = t.requestedByUserId
262
+ ORDER BY t.lastUpdatedAt DESC, t.createdAt DESC, t.id DESC`,
263
+ ids,
264
+ ),
265
+ getDbClient().query<{
266
+ root_id: string;
267
+ id: string | null;
268
+ name: string | null;
269
+ access: "read" | "write";
270
+ }>(
271
+ `SELECT sh.rootTaskId AS root_id, sh.userId AS id, u.name, sh.access FROM session_shares sh
272
+ LEFT JOIN users u ON u.id = sh.userId
273
+ WHERE sh.revokedAt IS NULL AND sh.rootTaskId IN (${ids.map(() => "?").join(",")})
274
+ ORDER BY sh.createdAt, sh.id`,
275
+ ids,
276
+ ),
277
+ ]);
278
+ const people = new Map<string, Map<string, string>>();
279
+ for (const root of roots) {
280
+ result.set(root.id, { shared: false, preview: null, shares: [] });
281
+ people.set(root.id, new Map());
282
+ }
283
+ // Chain rows are newest first. Reserve the requester slot before adding other people.
284
+ for (const root of roots) {
285
+ if (!root.requestedByUserId) continue;
286
+ people.get(root.id)!.set(root.requestedByUserId, root.requestedByUserId);
287
+ }
288
+ for (const task of tasks) {
289
+ if (task.id) people.get(task.root_id)!.set(task.id, task.name ?? task.id);
290
+ const fields = result.get(task.root_id)!;
291
+ if (fields.preview === null && task.preview !== null) fields.preview = task.preview;
292
+ }
293
+ for (const share of shares) {
294
+ const fields = result.get(share.root_id)!;
295
+ fields.shared = true;
296
+ fields.shares!.push({ userId: share.id, name: share.name, access: share.access });
297
+ if (share.id) people.get(share.root_id)!.set(share.id, share.name ?? share.id);
298
+ }
299
+ for (const [id, members] of people) {
300
+ Object.assign(result.get(id)!, {
301
+ people: [...members].slice(0, 8).map(([id, name]) => ({ id, name })),
302
+ peopleCount: members.size,
303
+ });
304
+ }
305
+ return result;
306
+ }
307
+
308
+ export interface SessionSectionCounts {
309
+ total: number;
310
+ unread: number;
311
+ running: number;
312
+ needsYou: number;
313
+ }
314
+ export interface SessionTreeGroup extends SessionSectionCounts {
315
+ key: string;
316
+ lastActivityAt: string;
317
+ }
318
+
319
+ export interface SessionTree {
320
+ groups: SessionTreeGroup[];
321
+ inbox: SessionSectionCounts;
322
+ later: SessionSectionCounts;
323
+ done: SessionSectionCounts;
324
+ noGroup: SessionSectionCounts;
325
+ }
326
+
327
+ export async function getSessionTree(
328
+ actor: { kind: "bypass" } | { kind: "user"; userId: string | null },
329
+ now?: string,
330
+ ): Promise<SessionTree> {
331
+ const query = sessionSummaryQuery({
332
+ ...(actor.kind === "user" ? { viewer: { userId: actor.userId, scope: "all" } as const } : {}),
333
+ now,
334
+ tree: true,
335
+ });
336
+ const rows = await getDbClient().query<{
337
+ key: string;
338
+ lastActivityAt: string;
339
+ latestStatus: string;
340
+ readState: SessionReadState | null;
341
+ needsYou: number;
342
+ }>(
343
+ `SELECT r."key", a.lastActivityAt, a.latestStatus, a.readState, a.needsYou
344
+ FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id`,
345
+ query.params,
346
+ );
347
+ const counts = (): SessionSectionCounts => ({ total: 0, unread: 0, running: 0, needsYou: 0 });
348
+ const result: SessionTree = {
349
+ groups: [],
350
+ inbox: counts(),
351
+ later: counts(),
352
+ done: counts(),
353
+ noGroup: counts(),
354
+ };
355
+ const groups = new Map<string, SessionTreeGroup>();
356
+ const add = (section: SessionSectionCounts, row: (typeof rows)[number]) => {
357
+ section.total++;
358
+ if (row.readState === "unread") section.unread++;
359
+ if (!isTerminalTaskStatus(row.latestStatus)) section.running++;
360
+ if (row.needsYou) section.needsYou++;
361
+ };
362
+ for (const row of rows) {
363
+ const key = sessionGroupKey(row.key);
364
+ const noGroup =
365
+ key === "shared/" || (actor.kind === "user" && key === `personal/${actor.userId}/`);
366
+ let group: SessionTreeGroup | undefined;
367
+ if (!noGroup) {
368
+ group = groups.get(key);
369
+ if (!group) {
370
+ group = { key, ...counts(), lastActivityAt: row.lastActivityAt };
371
+ groups.set(key, group);
372
+ }
373
+ if (row.lastActivityAt > group.lastActivityAt) group.lastActivityAt = row.lastActivityAt;
374
+ }
375
+ if (row.readState === "later" || row.readState === "done") add(result[row.readState], row);
376
+ else {
377
+ add(result.inbox, row);
378
+ if (noGroup) add(result.noGroup, row);
379
+ else if (group) add(group, row);
380
+ }
381
+ }
382
+ result.groups = [...groups.values()].sort((a, b) => a.key.localeCompare(b.key));
383
+ return result;
384
+ }