@desplega.ai/agent-swarm 2.7.4 → 2.7.6

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 (326) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-bp91d37f.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
  13. package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
  14. package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
  16. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  17. package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
  18. package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
  19. package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
  20. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  21. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  22. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  23. package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
  24. package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
  25. package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
  26. package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
  27. package/dist/cli-54t00gt8.js +151 -0
  28. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  29. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  30. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  31. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  32. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  33. package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
  34. package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
  35. package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
  36. package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
  37. package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
  38. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  39. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  40. package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
  41. package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
  42. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  43. package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
  44. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  45. package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
  47. package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
  48. package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
  49. package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
  50. package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
  51. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  52. package/dist/cli-fw2w0kva.js +47 -0
  53. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  54. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  55. package/dist/cli-gdhpcbhf.js +55 -0
  56. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  57. package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
  58. package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
  59. package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
  60. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  61. package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
  62. package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
  63. package/dist/cli-m2z8jagn.js +12078 -0
  64. package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
  65. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  66. package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
  67. package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
  68. package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
  69. package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
  70. package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
  71. package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
  72. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
  75. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  76. package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
  77. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  78. package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
  79. package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
  80. package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
  81. package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
  82. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  83. package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
  84. package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
  85. package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
  86. package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
  87. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  88. package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
  89. package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
  90. package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
  91. package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
  92. package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
  93. package/dist/cli.js +26 -25
  94. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
  95. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  96. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  97. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
  98. package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
  99. package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
  100. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
  101. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
  102. package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
  103. package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
  104. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  105. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  106. package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
  107. package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
  108. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
  109. package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
  110. package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
  111. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
  112. package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
  113. package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
  114. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  115. package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
  116. package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
  117. package/dist/fx-adapter-fy8rr921.js +18 -0
  118. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
  119. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
  120. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
  121. package/dist/handlers-by3sfxyx.js +32 -0
  122. package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
  123. package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
  124. package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
  125. package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
  126. package/dist/index-4f2pvxae.js +37 -0
  127. package/dist/index-98gz7tmp.js +30 -0
  128. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  129. package/dist/index-k08z2cak.js +33 -0
  130. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  131. package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
  132. package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
  133. package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
  134. package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
  135. package/dist/lead-enq35nqp.js +70 -0
  136. package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
  137. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
  138. package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
  139. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
  140. package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
  141. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
  142. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
  143. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
  144. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
  145. package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
  146. package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
  147. package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
  148. package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
  149. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
  150. package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
  151. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
  152. package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
  153. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
  154. package/dist/worker-gg1576j3.js +70 -0
  155. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  156. package/openapi.json +189 -32
  157. package/package.json +8 -7
  158. package/src/be/db/script-runs.ts +23 -0
  159. package/src/be/db/session-summaries.ts +8 -2
  160. package/src/be/db/session-tree.ts +174 -61
  161. package/src/be/db/session-visibility.ts +5 -4
  162. package/src/be/db/tasks/read.ts +122 -3
  163. package/src/be/db-client.ts +8 -5
  164. package/src/be/db.ts +23 -8
  165. package/src/be/memory/providers/sqlite-store.ts +49 -12
  166. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  167. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  168. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  169. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  170. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  171. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  172. package/src/be/model-validation.ts +5 -1
  173. package/src/be/pricing-normalize.ts +7 -2
  174. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  175. package/src/be/runtime-pools/db.ts +65 -6
  176. package/src/be/runtime-pools/remote-controller.ts +5 -0
  177. package/src/be/runtime-pools/supervisor.ts +33 -2
  178. package/src/be/scripts/typecheck.ts +23 -0
  179. package/src/be/seed/schedules-seeder.ts +135 -9
  180. package/src/be/seed/workflows-seeder.ts +65 -1
  181. package/src/be/seed-pricing.ts +23 -0
  182. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  183. package/src/be/steering.ts +50 -3
  184. package/src/be/swarm-config-guard.ts +1 -0
  185. package/src/commands/controller.ts +4 -0
  186. package/src/commands/entrypoint-api-readiness.ts +74 -0
  187. package/src/commands/provider-credentials.ts +29 -2
  188. package/src/commands/runner.ts +86 -5
  189. package/src/extensions/contract-types.generated.ts +1 -1
  190. package/src/extensions/contract.ts +27 -1
  191. package/src/extensions/dispatcher.ts +4 -0
  192. package/src/http/agents.ts +1 -0
  193. package/src/http/index.ts +1 -1
  194. package/src/http/list-total-cache.ts +14 -7
  195. package/src/http/mcp-bridge.ts +4 -0
  196. package/src/http/memory.ts +38 -9
  197. package/src/http/script-runs.ts +174 -5
  198. package/src/http/status.ts +10 -2
  199. package/src/http/tasks.ts +4 -2
  200. package/src/integrations/chat-sdk/inbound.ts +15 -5
  201. package/src/integrations/chat-sdk/steering.ts +3 -1
  202. package/src/kv-reserved-namespaces.ts +2 -0
  203. package/src/prompts/base-prompt.ts +4 -1
  204. package/src/prompts/session-templates.ts +26 -0
  205. package/src/providers/acp-adapter.ts +190 -26
  206. package/src/providers/acp-targets.ts +306 -6
  207. package/src/providers/amp-adapter.ts +1 -1
  208. package/src/providers/codex-adapter.ts +9 -44
  209. package/src/providers/codex-oauth/flow.ts +24 -3
  210. package/src/providers/codex-oauth/storage.ts +56 -17
  211. package/src/providers/codex-oauth/subscription.ts +67 -0
  212. package/src/providers/fx-adapter.ts +211 -0
  213. package/src/providers/index.ts +5 -1
  214. package/src/providers/mcp-loopback-proxy.ts +94 -0
  215. package/src/providers/pi-mono-adapter.ts +98 -13
  216. package/src/providers/pricing-sources.md +8 -0
  217. package/src/providers/reasoning-effort.ts +17 -1
  218. package/src/providers/types.ts +2 -1
  219. package/src/runtime-providers/controller-protocol.ts +14 -1
  220. package/src/runtime-providers/controller.ts +5 -0
  221. package/src/runtime-providers/docker.ts +108 -23
  222. package/src/runtime-providers/types.ts +16 -0
  223. package/src/scheduler/scheduler.ts +36 -3
  224. package/src/script-workflows/workflow-ctx.ts +8 -0
  225. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  226. package/src/scripts-runtime/types/stdlib.d.ts +27 -0
  227. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  228. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
  229. package/src/server-user.ts +1 -0
  230. package/src/server.ts +7 -0
  231. package/src/slack/assistant.ts +17 -1
  232. package/src/slack/commands.ts +5 -1
  233. package/src/slack/handlers.ts +53 -5
  234. package/src/slack/left-threads.ts +88 -0
  235. package/src/slack/steering.ts +3 -0
  236. package/src/slack/thread-buffer.ts +40 -16
  237. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  238. package/src/tests/acp-adapter.test.ts +79 -0
  239. package/src/tests/base-prompt.test.ts +12 -0
  240. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  241. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  242. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  243. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  244. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  245. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  246. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  247. package/src/tests/context-window.test.ts +7 -0
  248. package/src/tests/db-client.test.ts +21 -0
  249. package/src/tests/db-task-read-contract.test.ts +8 -2
  250. package/src/tests/db-task-write-contract.test.ts +5 -1
  251. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  252. package/src/tests/explicit-model-validation.test.ts +113 -8
  253. package/src/tests/extensions-run-context.test.ts +11 -0
  254. package/src/tests/extensions-steering.test.ts +353 -0
  255. package/src/tests/extensions-validate.test.ts +5 -0
  256. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  257. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  258. package/src/tests/fixtures/fake-fx.ts +167 -0
  259. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  260. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  261. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  262. package/src/tests/fx-adapter.test.ts +771 -0
  263. package/src/tests/grok-adapter.test.ts +36 -0
  264. package/src/tests/list-total-cache.test.ts +27 -2
  265. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  266. package/src/tests/memory-fts-populate.test.ts +11 -9
  267. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  268. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  269. package/src/tests/model-tier-resolution.test.ts +3 -3
  270. package/src/tests/pricing-refresh.test.ts +5 -5
  271. package/src/tests/prompt-template-session.test.ts +5 -3
  272. package/src/tests/provider-registration.test.ts +2 -1
  273. package/src/tests/query-audit-plans.test.ts +30 -2
  274. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  275. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  276. package/src/tests/runtime-pools-db.test.ts +98 -0
  277. package/src/tests/runtime-pools-http.test.ts +29 -0
  278. package/src/tests/runtime-providers-docker.test.ts +253 -0
  279. package/src/tests/runtime-remote-controller.test.ts +66 -2
  280. package/src/tests/runtime-supervisor.test.ts +66 -2
  281. package/src/tests/script-connections.test.ts +13 -0
  282. package/src/tests/script-runs-http.test.ts +684 -2
  283. package/src/tests/seed-model-profiles.test.ts +2 -2
  284. package/src/tests/seed-schedules.test.ts +138 -0
  285. package/src/tests/seed-scripts.test.ts +26 -0
  286. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  287. package/src/tests/session-costs-golden.test.ts +26 -0
  288. package/src/tests/session-summaries-plans.test.ts +120 -0
  289. package/src/tests/session-summaries.test.ts +72 -12
  290. package/src/tests/sessions-tree.test.ts +11 -3
  291. package/src/tests/slack-leave-thread.test.ts +571 -0
  292. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  293. package/src/tests/status.test.ts +11 -0
  294. package/src/tests/tasks-readable-row.test.ts +155 -0
  295. package/src/tests/tool-annotations.test.ts +2 -1
  296. package/src/tests/workflow-engine-v2.test.ts +42 -0
  297. package/src/tests/workflow-script-principal.test.ts +207 -7
  298. package/src/tools/send-task.ts +35 -47
  299. package/src/tools/slack-leave-thread.ts +104 -0
  300. package/src/tools/steer-task.ts +3 -2
  301. package/src/tools/task-dedup.ts +45 -21
  302. package/src/tools/tool-config.ts +2 -1
  303. package/src/types.ts +57 -0
  304. package/src/utils/codex-auth-failure.ts +30 -0
  305. package/src/utils/context-window.ts +4 -1
  306. package/src/utils/credentials.ts +7 -0
  307. package/src/utils/fx-models.ts +43 -0
  308. package/src/workflows/cooldown.ts +3 -2
  309. package/src/workflows/engine.ts +24 -9
  310. package/templates/schedules/dreaming-daily/config.json +1 -1
  311. package/templates/schedules/dreaming-daily/content.md +6 -14
  312. package/templates/skills/composio-google-docs/content.md +10 -0
  313. package/templates/skills/script-workflows/content.md +19 -1
  314. package/templates/skills/swarm-docs/content.md +2 -0
  315. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  316. package/templates/skills/swarm-scripts/content.md +2 -0
  317. package/templates/workflows/dreaming/content.md +5 -1
  318. package/dist/acp-adapter-96dc64m2.js +0 -15
  319. package/dist/cli-dznrgaep.js +0 -30
  320. package/dist/cli-w3hcrxht.js +0 -6985
  321. package/dist/handlers-jg7v74ty.js +0 -31
  322. package/dist/index-8c1zbz59.js +0 -29
  323. package/dist/index-8k8tmfxd.js +0 -32
  324. package/dist/index-hq617xdj.js +0 -36
  325. package/dist/lead-567kw7x3.js +0 -68
  326. package/dist/worker-fjdqra6q.js +0 -68
@@ -70,10 +70,27 @@ type SqlFragment = { sql: string; params: (string | number | null)[] };
70
70
  /** How long a finished, read Inbox session stays quiet before it moves to Done on its own. */
71
71
  const AUTO_DONE_QUIET_MS = 24 * 3600000;
72
72
 
73
+ /** The chain aggregates the read-state rule reads, as SQL expressions. */
74
+ interface ReadStateColumns {
75
+ lastActivityAt: string;
76
+ openTasks: string;
77
+ pendingApproval: string;
78
+ /** Whether the chain changed in a way that reopens a Done session; read only for a Done row `s`. */
79
+ reopensDone: string;
80
+ }
81
+
82
+ /** The columns of the chain walk's `agg` rows (`legacySessionSummaryQuery`). */
83
+ const LEGACY_READ_STATE_COLUMNS: ReadStateColumns = {
84
+ lastActivityAt: "a.lastActivityAt",
85
+ openTasks: "a.openTasks",
86
+ pendingApproval: "a.pendingApproval",
87
+ reopensDone: "a.reopensDone > 0",
88
+ };
89
+
73
90
  /**
74
91
  * Shared read-state rule with bound ISO timestamps for the list and tree.
75
92
  * Done holds through agent activity (progress, status changes, child tasks):
76
- * only `a.reopensDone`, counted in `sessionSummaryQuery`, brings it back.
93
+ * only `reopensDone` brings it back.
77
94
  *
78
95
  * Auto-Done: an Inbox session (`inboxScopeCondition` on its root `r`) reads
79
96
  * as Done once it reads as read, every chain task is finished, no approval
@@ -85,21 +102,22 @@ const AUTO_DONE_QUIET_MS = 24 * 3600000;
85
102
  function sessionReadStateSql(
86
103
  userId: string | null | undefined,
87
104
  now = new Date().toISOString(),
105
+ a: ReadStateColumns = LEGACY_READ_STATE_COLUMNS,
88
106
  ): SqlFragment {
89
107
  const quiet = new Date(Date.parse(now) - AUTO_DONE_QUIET_MS).toISOString();
90
108
  const scope = inboxScopeCondition(userId);
91
- const autoDone = `(a.openTasks = 0 AND a.pendingApproval = 0 AND a.lastActivityAt <= ? AND ${scope.sql})`;
109
+ const autoDone = `(${a.openTasks} = 0 AND ${a.pendingApproval} = 0 AND ${a.lastActivityAt} <= ? AND ${scope.sql})`;
92
110
  const autoDoneParams = [quiet, ...scope.params];
93
111
  return {
94
112
  sql: `CASE
95
113
  WHEN s.id IS NULL THEN CASE
96
- WHEN a.lastActivityAt > ? THEN 'unread'
114
+ WHEN ${a.lastActivityAt} > ? THEN 'unread'
97
115
  WHEN ${autoDone} THEN 'done'
98
116
  ELSE 'read' END
99
117
  WHEN s.status = 'dismissed' THEN 'done'
100
- WHEN s.status = 'done' THEN CASE WHEN a.reopensDone > 0 THEN 'unread' ELSE 'done' END
118
+ WHEN s.status = 'done' THEN CASE WHEN ${a.reopensDone} THEN 'unread' ELSE 'done' END
101
119
  WHEN s.status = 'snoozed' THEN CASE WHEN s.snoozeUntil > ? THEN 'later' ELSE 'unread' END
102
- WHEN a.lastActivityAt > COALESCE(s.lastReadAt, s.lastUpdatedAt) THEN 'unread'
120
+ WHEN ${a.lastActivityAt} > COALESCE(s.lastReadAt, s.lastUpdatedAt) THEN 'unread'
103
121
  WHEN ${autoDone} AND s.lastUpdatedAt <= ? THEN 'done'
104
122
  ELSE 'read'
105
123
  END`,
@@ -117,22 +135,42 @@ function sourceKeyRanges() {
117
135
  return SESSION_SOURCES.map((source) => ({ source, ...keyPrefixRange(`shared/${source}/`) }));
118
136
  }
119
137
 
120
- /** Count every owned root at the exact source thread key, independent of viewer filters. */
121
- export function threadRootsSql(): SqlFragment {
138
+ /** Whether `r."key"` is a source thread key (`isSourceThreadKey`). */
139
+ function sourceThreadKeySql(): SqlFragment {
122
140
  const key = 'r."key"';
123
141
  // Trim the final slash, then all non-slash characters to locate the last segment.
124
142
  // For example, shared/slack/c1/thread:1.2/ gives thread:1.2/.
125
143
  const leaf = `substr(${key}, length(rtrim(rtrim(${key}, '/'), replace(${key}, '/', ''))) + 1)`;
126
144
  const ranges = sourceKeyRanges();
127
145
  return {
128
- sql: `(CASE WHEN (${ranges.map(() => `(${key} >= ? AND ${key} < ?)`).join(" OR ")}) AND ${leaf} GLOB 'thread:?*/'
129
- THEN (SELECT COUNT(*) FROM agent_tasks session_thread_root WHERE session_thread_root."key" = ${key}
130
- AND session_thread_root.parentTaskId IS NULL AND session_thread_root.requestedByUserId IS NOT NULL)
131
- ELSE 0 END)`,
146
+ sql: `(${ranges.map(() => `(${key} >= ? AND ${key} < ?)`).join(" OR ")}) AND ${leaf} GLOB 'thread:?*/'`,
132
147
  params: ranges.flatMap(({ lo, hi }) => [lo, hi]),
133
148
  };
134
149
  }
135
150
 
151
+ /** Count every owned root at the exact source thread key, independent of viewer filters. */
152
+ export function threadRootsSql(): SqlFragment {
153
+ const thread = sourceThreadKeySql();
154
+ return {
155
+ sql: `(CASE WHEN ${thread.sql}
156
+ THEN (SELECT COUNT(*) FROM session_summaries session_thread_root
157
+ WHERE session_thread_root."key" = r."key" AND session_thread_root.requestedByUserId IS NOT NULL)
158
+ ELSE 0 END)`,
159
+ params: thread.params,
160
+ };
161
+ }
162
+
163
+ /** {@link threadRootsSql} for every source thread key at once: one grouped read for the tree. */
164
+ async function threadRootCounts(): Promise<Map<string, number>> {
165
+ const thread = sourceThreadKeySql();
166
+ const rows = await getDbClient().query<{ key: string; threadRoots: number }>(
167
+ `SELECT r."key", COUNT(*) AS threadRoots FROM session_summaries r
168
+ WHERE r.requestedByUserId IS NOT NULL AND ${thread.sql} GROUP BY r."key"`,
169
+ thread.params,
170
+ );
171
+ return new Map(rows.map((row) => [row.key, row.threadRoots]));
172
+ }
173
+
136
174
  /** A task's group with the list row's rule (`groupKey` on `GET /api/sessions`). */
137
175
  export async function getSessionGroupKey(task: { id: string; key: string }): Promise<string> {
138
176
  const threadRoots = threadRootsSql();
@@ -143,14 +181,19 @@ export async function getSessionGroupKey(task: { id: string; key: string }): Pro
143
181
  return sessionThreadGroupKey(task.key, row?.threadRoots ?? 0);
144
182
  }
145
183
 
146
- function groupCondition(group: string): SqlFragment {
184
+ /**
185
+ * `scan` keeps the key index out of the range (unary `+`). A broad prefix such
186
+ * as `shared/` holds most roots, where one pass over the narrow summaries
187
+ * beats an index lookup per row; without planner statistics SQLite cannot tell.
188
+ */
189
+ function groupCondition(group: string, scan = false): SqlFragment {
147
190
  const { lo, hi } = keyPrefixRange(group);
148
191
  // The remaining key must be one resource leaf, with no nested path.
149
192
  // SQLite substr/length count code points, so count them here too (not UTF-16 units).
150
193
  const start = [...lo].length + 1;
151
194
  const threadRoots = threadRootsSql();
152
195
  return {
153
- sql: `(r."key" >= ? AND r."key" < ? AND (r."key" = ? OR (
196
+ sql: `(${scan ? "+" : ""}r."key" >= ? AND ${scan ? "+" : ""}r."key" < ? AND (r."key" = ? OR (
154
197
  instr(substr(r."key", ?, length(r."key") - ?), '/') = 0 AND
155
198
  (${SESSION_LEAF_KINDS.map(() => 'substr(r."key", ?) GLOB ?').join(" OR ")}) AND
156
199
  ${threadRoots.sql} < ?
@@ -170,8 +213,8 @@ function groupCondition(group: string): SqlFragment {
170
213
 
171
214
  /** No group: roots at `shared/` and at the viewer's `personal/<me>/`, with their direct leaves. */
172
215
  function noGroupCondition(userId: string | null | undefined): SqlFragment {
173
- const groups = [groupCondition("shared/")];
174
- if (userId) groups.push(groupCondition(`personal/${userId}/`));
216
+ const groups = [groupCondition("shared/", true)];
217
+ if (userId) groups.push(groupCondition(`personal/${userId}/`, true));
175
218
  return {
176
219
  sql: `(${groups.map((g) => g.sql).join(" OR ")})`,
177
220
  params: groups.flatMap((g) => g.params),
@@ -187,7 +230,7 @@ function inboxScopeCondition(userId: string | null | undefined): SqlFragment {
187
230
  const ranges = sourceKeyRanges();
188
231
  const noGroup = noGroupCondition(userId);
189
232
  return {
190
- sql: `(${ranges.map(() => '(r."key" >= ? AND r."key" < ?)').join(" OR ")} OR ${noGroup.sql})`,
233
+ sql: `(${ranges.map(() => '(+r."key" >= ? AND +r."key" < ?)').join(" OR ")} OR ${noGroup.sql})`,
191
234
  params: [...ranges.flatMap(({ lo, hi }) => [lo, hi]), ...noGroup.params],
192
235
  };
193
236
  }
@@ -201,8 +244,19 @@ function effectiveSourceSql(): SqlFragment {
201
244
  };
202
245
  }
203
246
 
247
+ /**
248
+ * One row per session root: `session_summaries` (migration 253) with the root
249
+ * id as `id`, so the root filters below apply to it as they would to the root
250
+ * task. SQLite flattens the subquery, so the summary indexes still serve them.
251
+ */
252
+ const SESSION_ROOTS = "(SELECT rootId AS id, * FROM session_summaries)";
253
+
254
+ /**
255
+ * Conditions on a session root `r`: a {@link SESSION_ROOTS} row or a root task
256
+ * (`legacySessionSummaryQuery`). Only `q` reads the root task itself.
257
+ */
204
258
  export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
205
- const conditions = ["r.parentTaskId IS NULL"];
259
+ const conditions: string[] = [];
206
260
  const params: SqlFragment["params"] = [];
207
261
  const add = (fragment: SqlFragment) => {
208
262
  conditions.push(fragment.sql);
@@ -235,7 +289,8 @@ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
235
289
  if (opts.q?.trim()) {
236
290
  const like = `%${opts.q.trim().toLowerCase()}%`;
237
291
  add({
238
- sql: "(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)",
292
+ sql: `EXISTS (SELECT 1 FROM agent_tasks rt WHERE rt.id = r.id
293
+ AND (lower(rt.task) LIKE ? OR lower(COALESCE(rt.title, '')) LIKE ?))`,
239
294
  params: [like, like],
240
295
  });
241
296
  }
@@ -248,7 +303,8 @@ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
248
303
  });
249
304
  if (opts.viewer) {
250
305
  add(sessionViewerCondition(opts.viewer));
251
- if (opts.tree) conditions.push("r.requestedByUserId IS NOT NULL");
306
+ // Unary +: a full pass over the narrow summaries beats the partial thread index here.
307
+ if (opts.tree) conditions.push("+r.requestedByUserId IS NOT NULL");
252
308
  if (opts.mine) add(sessionMineCondition(opts.viewer.userId));
253
309
  }
254
310
  if (opts.keyPrefix) {
@@ -272,7 +328,7 @@ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
272
328
  : marked,
273
329
  );
274
330
  }
275
- return { sql: conditions.join(" AND "), params };
331
+ return { sql: conditions.length ? conditions.join(" AND ") : "1", params };
276
332
  }
277
333
 
278
334
  function excludesTriaged(opts: SessionFilters): boolean {
@@ -316,19 +372,9 @@ const reopensDoneSql = `(d.id IS NOT NULL AND (
316
372
  WHERE ar.sourceTaskId = chain.id AND ar.status = 'pending' AND ar.createdAt > d.lastUpdatedAt)
317
373
  ))`;
318
374
 
319
- /**
320
- * A session starts at a parentTaskId IS NULL root. One recursive CTE computes
321
- * chain-wide latest activity for list ordering. Root filters precede recursion.
322
- * Read state and chain filters follow aggregation.
323
- */
324
- export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
325
- const root = sessionRootFilter(opts);
326
- const read = opts.viewer?.userId
327
- ? sessionReadStateSql(opts.viewer.userId, opts.now)
328
- : { sql: "NULL", params: [] };
329
- const params = [...root.params];
330
- if (opts.withUserId) params.push(opts.withUserId);
331
- params.push(opts.viewer?.userId ?? null, ...read.params, opts.viewer?.userId ?? null);
375
+ /** Filters on summary rows `a` (read state, latest status, participants), after aggregation. */
376
+ function summaryRowFilters(opts: SessionFilters): SqlFragment {
377
+ const params: SqlFragment["params"] = [];
332
378
  const filters: string[] = [];
333
379
  if (opts.triage === "unread") filters.push("readState = 'unread'");
334
380
  else if (opts.triage === "later" || opts.triage === "done") {
@@ -345,6 +391,73 @@ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
345
391
  WHERE sh.rootTaskId = a.root_id AND sh.revokedAt IS NULL AND sh.userId = ?))`);
346
392
  params.push(opts.withUserId);
347
393
  }
394
+ return { sql: filters.length ? `WHERE ${filters.join(" AND ")}` : "", params };
395
+ }
396
+
397
+ /**
398
+ * One row per visible session root, with its chain aggregates, the viewer's
399
+ * read state and `needsYou`. It reads the trigger-kept `session_summaries`, so
400
+ * no request walks a chain: root filters, then one primary-key join for the
401
+ * viewer's triage row. Same rule and columns as {@link legacySessionSummaryQuery},
402
+ * plus the root columns the list and tree order and group by.
403
+ */
404
+ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
405
+ const root = sessionRootFilter(opts);
406
+ const read = opts.viewer?.userId
407
+ ? sessionReadStateSql(opts.viewer.userId, opts.now, {
408
+ lastActivityAt: "r.lastActivityAt",
409
+ openTasks: "r.openTaskCount",
410
+ pendingApproval: "r.pendingApprovalCount",
411
+ // The newest human event is newer than the Done mark (`reopensDoneSql`).
412
+ reopensDone: "r.lastHumanAt > s.lastUpdatedAt",
413
+ })
414
+ : { sql: "NULL", params: [] };
415
+ const filters = summaryRowFilters(opts);
416
+ // Sessions where the user requested any chain task, from the requester index.
417
+ const withUser = opts.withUserId
418
+ ? `r.id IN (SELECT m.rootId FROM agent_tasks t JOIN session_members m ON m.taskId = t.id
419
+ WHERE t.requestedByUserId = ?) AS withUser,`
420
+ : "";
421
+ return {
422
+ sql: `WITH state AS (
423
+ SELECT r.id AS root_id, r.createdAt AS rootCreatedAt, r."key", r.source, r.slackChannelId,
424
+ r.chainCount, r.latestStatus, r.lastActivityAt, ${withUser}
425
+ r.pendingApprovalCount AS pendingApproval, r.openTaskCount AS openTasks,
426
+ ${read.sql} AS readState, s.snoozeUntil
427
+ FROM ${SESSION_ROOTS} r LEFT JOIN inbox_item_state s
428
+ ON s.userId = ? AND s.itemType = 'to_read' AND s.itemId = r.id
429
+ WHERE ${root.sql}
430
+ ), summary AS (
431
+ SELECT *, (COALESCE(readState = 'unread' AND latestStatus = 'failed', 0) OR pendingApproval) AS needsYou
432
+ FROM state
433
+ )
434
+ SELECT a.* FROM summary a ${filters.sql}`,
435
+ params: [
436
+ ...(opts.withUserId ? [opts.withUserId] : []),
437
+ ...read.params,
438
+ opts.viewer?.userId ?? null,
439
+ ...root.params,
440
+ ...filters.params,
441
+ ],
442
+ };
443
+ }
444
+
445
+ /**
446
+ * The chain walk the summaries replace: a session starts at a parentTaskId IS
447
+ * NULL root, and one recursive CTE computes chain-wide latest activity. Root
448
+ * filters precede recursion; read state and chain filters follow aggregation.
449
+ * Off the request path: tests compare {@link sessionSummaryQuery} with it.
450
+ */
451
+ export function legacySessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
452
+ const root = sessionRootFilter(opts);
453
+ const read = opts.viewer?.userId
454
+ ? sessionReadStateSql(opts.viewer.userId, opts.now)
455
+ : { sql: "NULL", params: [] };
456
+ const filters = summaryRowFilters(opts);
457
+ const params = [...root.params];
458
+ if (opts.withUserId) params.push(opts.withUserId);
459
+ params.push(opts.viewer?.userId ?? null, ...read.params, opts.viewer?.userId ?? null);
460
+ params.push(...filters.params);
348
461
  return {
349
462
  // SQLite takes the bare status from the row with MAX(lastUpdatedAt).
350
463
  // Keep this as the only MIN/MAX aggregate so status follows activity.
@@ -354,7 +467,7 @@ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
354
467
  createdAt, source, requestedByUserIdInherited) AS (
355
468
  SELECT r.id, r.id, r.lastUpdatedAt, r.status, r.requestedByUserId,
356
469
  r.createdAt, r.source, r.requestedByUserIdInherited
357
- FROM agent_tasks r WHERE ${root.sql}
470
+ FROM agent_tasks r WHERE r.parentTaskId IS NULL AND ${root.sql}
358
471
  UNION ALL
359
472
  SELECT c.root_id, t.id, t.lastUpdatedAt, t.status, t.requestedByUserId,
360
473
  t.createdAt, t.source, t.requestedByUserIdInherited
@@ -378,7 +491,7 @@ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
378
491
  SELECT *, (COALESCE(readState = 'unread' AND latestStatus = 'failed', 0) OR pendingApproval) AS needsYou
379
492
  FROM state
380
493
  )
381
- SELECT a.* FROM summary a ${filters.length ? `WHERE ${filters.join(" AND ")}` : ""}`,
494
+ SELECT a.* FROM summary a ${filters.sql}`,
382
495
  params,
383
496
  };
384
497
  }
@@ -407,7 +520,7 @@ export function sessionCountQuery(opts: SessionFilters = {}): SqlFragment {
407
520
  return {
408
521
  sql: chain
409
522
  ? `SELECT COUNT(*) AS count FROM (${query.sql})`
410
- : `SELECT COUNT(*) AS count FROM agent_tasks r WHERE ${query.sql}`,
523
+ : `SELECT COUNT(*) AS count FROM ${SESSION_ROOTS} r WHERE ${query.sql}`,
411
524
  params: query.params,
412
525
  };
413
526
  }
@@ -439,13 +552,11 @@ export async function sessionPageFields(
439
552
  name: string | null;
440
553
  preview: string | null;
441
554
  }>(
442
- `WITH RECURSIVE chain(root_id, id) AS (
443
- SELECT id, id FROM agent_tasks WHERE id IN (${ids.map(() => "?").join(",")})
444
- UNION ALL SELECT c.root_id, t.id FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
445
- ) SELECT c.root_id, t.requestedByUserId AS id, u.name,
555
+ `SELECT m.rootId AS root_id, t.requestedByUserId AS id, u.name,
446
556
  CASE WHEN t.source = 'system' AND t.taskType = 'follow-up' THEN NULL ELSE NULLIF(trim(substr(t.output, 1, 200)), '') END AS preview
447
- FROM chain c CROSS JOIN agent_tasks t ON t.id = c.id
557
+ FROM session_members m CROSS JOIN agent_tasks t ON t.id = m.taskId
448
558
  LEFT JOIN users u ON u.id = t.requestedByUserId
559
+ WHERE m.rootId IN (${ids.map(() => "?").join(",")})
449
560
  ORDER BY t.lastUpdatedAt DESC, t.createdAt DESC, t.id DESC`,
450
561
  ids,
451
562
  ),
@@ -563,23 +674,24 @@ export async function getSessionTree(
563
674
  tree: true,
564
675
  mine: opts.mine,
565
676
  });
566
- const threadRoots = threadRootsSql();
567
- const rows = await getDbClient().query<{
568
- id: string;
569
- key: string;
570
- source: string;
571
- slackChannelId: string | null;
572
- lastActivityAt: string;
573
- latestStatus: string;
574
- readState: SessionReadState | null;
575
- needsYou: number;
576
- threadRoots: number;
577
- }>(
578
- `SELECT r.id, r."key", r.source, r.slackChannelId, a.lastActivityAt, a.latestStatus, a.readState, a.needsYou,
579
- ${threadRoots.sql} AS threadRoots
580
- FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id`,
581
- [...threadRoots.params, ...query.params],
582
- );
677
+ const [rows, threadRoots] = await Promise.all([
678
+ getDbClient().query<{
679
+ root_id: string;
680
+ key: string;
681
+ source: string;
682
+ slackChannelId: string | null;
683
+ lastActivityAt: string;
684
+ latestStatus: string;
685
+ readState: SessionReadState | null;
686
+ needsYou: number;
687
+ }>(
688
+ `SELECT a.root_id, a."key", a.source, a.slackChannelId, a.lastActivityAt, a.latestStatus,
689
+ a.readState, a.needsYou
690
+ FROM (${query.sql}) a`,
691
+ query.params,
692
+ ),
693
+ threadRootCounts(),
694
+ ]);
583
695
  const counts = (): SessionSectionCounts => ({ total: 0, unread: 0, running: 0, needsYou: 0 });
584
696
  const result: SessionTree = {
585
697
  groups: [],
@@ -608,14 +720,15 @@ export async function getSessionTree(
608
720
  for (const row of rows) {
609
721
  const source = effectiveSource(row.key, row.source);
610
722
  sources.set(source, (sources.get(source) ?? 0) + 1);
611
- const key = sessionThreadGroupKey(row.key, row.threadRoots);
723
+ const rowThreadRoots = threadRoots.get(row.key) ?? 0;
724
+ const key = sessionThreadGroupKey(row.key, rowThreadRoots);
612
725
  const sourceGroupKey = sessionGroupKey(row.key);
613
726
  const channelId = /^shared\/slack\/([^/]+)\/$/.exec(sourceGroupKey)?.[1];
614
727
  if (channelId && row.slackChannelId?.toLowerCase() === channelId) {
615
728
  slackChannels.add(channelId);
616
729
  }
617
- if (row.threadRoots >= NESTED_THREAD_MIN_ROOTS && isSourceThreadKey(row.key)) {
618
- threadRootIds.push(row.id);
730
+ if (rowThreadRoots >= NESTED_THREAD_MIN_ROOTS && isSourceThreadKey(row.key)) {
731
+ threadRootIds.push(row.root_id);
619
732
  sourceGroups.add(sourceGroupKey);
620
733
  }
621
734
  if (sourceOfKey(key)) sourceGroups.add(key);
@@ -13,8 +13,9 @@ export function sessionViewerCondition(viewer: SessionViewer): {
13
13
  sql: string;
14
14
  params: (string | null)[];
15
15
  } {
16
- const shared = `EXISTS (SELECT 1 FROM session_shares s
17
- WHERE s.rootTaskId = r.id AND s.revokedAt IS NULL AND (s.userId IS NULL OR s.userId = ?))`;
16
+ // Uncorrelated, so SQLite builds the shared set once instead of probing per root.
17
+ const shared = `r.id IN (SELECT s.rootTaskId FROM session_shares s
18
+ WHERE s.revokedAt IS NULL AND (s.userId IS NULL OR s.userId = ?))`;
18
19
  if (viewer.scope === "mine") {
19
20
  return { sql: "r.requestedByUserId = ?", params: [viewer.userId] };
20
21
  }
@@ -55,8 +56,8 @@ export function sessionMineCondition(userId: string | null): {
55
56
  return {
56
57
  sql: `((r.requestedByUserId = ? AND (r.source NOT IN (${machine})
57
58
  OR (r.source = 'system' AND r.taskType IN (${humanSystem}))))
58
- OR EXISTS (SELECT 1 FROM session_shares s
59
- WHERE s.rootTaskId = r.id AND s.revokedAt IS NULL AND (s.userId IS NULL OR s.userId = ?)))`,
59
+ OR r.id IN (SELECT s.rootTaskId FROM session_shares s
60
+ WHERE s.revokedAt IS NULL AND (s.userId IS NULL OR s.userId = ?)))`,
60
61
  params: [userId, ...MACHINE_SESSION_SOURCES, ...HUMAN_SYSTEM_TASK_TYPES, userId],
61
62
  };
62
63
  }
@@ -869,6 +869,52 @@ export function readableSessionCondition(
869
869
  };
870
870
  }
871
871
 
872
+ /** Parent hops the row-at-a-time walk takes before it falls back to the set form. */
873
+ const READABLE_WALK_MAX_DEPTH = 64;
874
+
875
+ /**
876
+ * The row-at-a-time form of {@link readableSessionCondition}, for a query that
877
+ * stops at a LIMIT. It walks up from each row it tests to the root, so a page
878
+ * costs a few key lookups per row instead of the set of every hidden task,
879
+ * which on a large DB took hundreds of milliseconds per request. The rule is
880
+ * the same: the root is the first task whose parent is null or missing. A row
881
+ * whose walk finds no root within {@link READABLE_WALK_MAX_DEPTH} hops (a very
882
+ * deep chain, or a parent cycle written before migration 253) falls back to
883
+ * the set form, which SQLite then builds once for the statement. A count reads
884
+ * every row, so it keeps the set form.
885
+ */
886
+ function readableSessionRowCondition(
887
+ userId: string | null,
888
+ taskIdColumn = "agent_tasks.id",
889
+ ): { sql: string; params: string[] } {
890
+ const otherOwner = userId
891
+ ? "up.requestedByUserId IS NOT NULL AND up.requestedByUserId != ?"
892
+ : "up.requestedByUserId IS NOT NULL";
893
+ const shareMatch = userId ? "(s.userId IS NULL OR s.userId = ?)" : "s.userId IS NULL";
894
+ const set = readableSessionCondition(userId, taskIdColumn);
895
+ return {
896
+ sql: `(CASE (
897
+ WITH RECURSIVE up(id, parentTaskId, requestedByUserId, scheduleId, depth) AS (
898
+ SELECT u.id, u.parentTaskId, u.requestedByUserId, u.scheduleId, 0
899
+ FROM agent_tasks u WHERE u.id = ${taskIdColumn}
900
+ UNION ALL
901
+ SELECT p.id, p.parentTaskId, p.requestedByUserId, p.scheduleId, up.depth + 1
902
+ FROM up JOIN agent_tasks p ON p.id = up.parentTaskId
903
+ WHERE up.depth < ${READABLE_WALK_MAX_DEPTH}
904
+ )
905
+ SELECT CASE WHEN ${otherOwner} AND up.scheduleId IS NULL
906
+ AND NOT EXISTS (SELECT 1 FROM session_shares s
907
+ WHERE s.rootTaskId = up.id AND s.revokedAt IS NULL AND ${shareMatch})
908
+ THEN 1 ELSE 0 END
909
+ FROM up
910
+ WHERE up.parentTaskId IS NULL
911
+ OR NOT EXISTS (SELECT 1 FROM agent_tasks p WHERE p.id = up.parentTaskId)
912
+ LIMIT 1
913
+ ) WHEN 0 THEN 1 WHEN 1 THEN 0 ELSE ${set.sql} END)`,
914
+ params: [...(userId ? [userId, userId] : []), ...set.params],
915
+ };
916
+ }
917
+
872
918
  export function getAllTasks(filters?: TaskFilters): Promise<AgentTask[]>;
873
919
  export function getAllTasks(
874
920
  filters: TaskFilters | undefined,
@@ -988,7 +1034,8 @@ export async function getAllTasks(
988
1034
  }
989
1035
 
990
1036
  if (filters?.readableBy) {
991
- const readable = readableSessionCondition(filters.readableBy.userId);
1037
+ // The page stops at LIMIT, so test only the rows it reads.
1038
+ const readable = readableSessionRowCondition(filters.readableBy.userId);
992
1039
  conditions.push(readable.sql);
993
1040
  params.push(...readable.params);
994
1041
  }
@@ -1865,7 +1912,44 @@ export async function getMostRecentTaskInThread(
1865
1912
  return row ? rowToAgentTask(row) : null;
1866
1913
  }
1867
1914
 
1868
- export async function findCompletedTaskInThread(
1915
+ /**
1916
+ * One page of completed tasks in a Slack thread, most recent first. Lead
1917
+ * `follow-up` sessions are excluded: they coordinate the work, they are not the
1918
+ * work, so they never count as "the request was already handled". The caller
1919
+ * matches descriptions in JS, so it pages through the whole window with the
1920
+ * `before` keyset cursor (the last row of the previous page) instead of
1921
+ * loading every row in one query.
1922
+ */
1923
+ export async function findCompletedTasksInThread(
1924
+ channelId: string,
1925
+ threadTs: string,
1926
+ windowMinutes: number,
1927
+ limit = 200,
1928
+ before?: { lastUpdatedAt: string; id: string },
1929
+ ): Promise<AgentTask[]> {
1930
+ const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
1931
+ const cursorSql = before ? "AND (lastUpdatedAt < ? OR (lastUpdatedAt = ? AND id < ?))" : "";
1932
+ const cursorParams = before ? [before.lastUpdatedAt, before.lastUpdatedAt, before.id] : [];
1933
+ const rows = await getDbClient().query<AgentTaskRow>(
1934
+ `SELECT * FROM agent_tasks
1935
+ WHERE slackChannelId = ?
1936
+ AND slackThreadTs = ?
1937
+ AND status = 'completed'
1938
+ AND COALESCE(taskType, '') != 'follow-up'
1939
+ AND lastUpdatedAt > ?
1940
+ ${cursorSql}
1941
+ ORDER BY lastUpdatedAt DESC, id DESC
1942
+ LIMIT ?`,
1943
+ [channelId, threadTs, since, ...cursorParams, limit],
1944
+ );
1945
+ return rows.map(rowToAgentTask);
1946
+ }
1947
+
1948
+ /**
1949
+ * Most recent FAILED task in a Slack thread. A failure more recent than a
1950
+ * completion means the follow-up re-delegation guard must let a retry through.
1951
+ */
1952
+ export async function findRecentFailedTaskInThread(
1869
1953
  channelId: string,
1870
1954
  threadTs: string,
1871
1955
  windowMinutes: number,
@@ -1875,7 +1959,7 @@ export async function findCompletedTaskInThread(
1875
1959
  `SELECT * FROM agent_tasks
1876
1960
  WHERE slackChannelId = ?
1877
1961
  AND slackThreadTs = ?
1878
- AND status = 'completed'
1962
+ AND status = 'failed'
1879
1963
  AND lastUpdatedAt > ?
1880
1964
  ORDER BY lastUpdatedAt DESC
1881
1965
  LIMIT 1`,
@@ -1884,6 +1968,41 @@ export async function findCompletedTaskInThread(
1884
1968
  return row ? rowToAgentTask(row) : null;
1885
1969
  }
1886
1970
 
1971
+ /**
1972
+ * Count the dispatches one agent made into a Slack thread since the window
1973
+ * start: each `send-task` task counts once, and each durable script run counts
1974
+ * once however many `ctx.step.agentTask` steps it created (step tasks share the
1975
+ * `script-run:<runId>:` contextKey prefix). A run is dated by its first step,
1976
+ * the dispatch the guard checked, so a run that started before the window does
1977
+ * not count again for steps it adds later. The follow-up re-delegation loop cap
1978
+ * reads this.
1979
+ */
1980
+ export async function countAgentDispatchesInThread(
1981
+ channelId: string,
1982
+ threadTs: string,
1983
+ creatorAgentId: string,
1984
+ windowMinutes: number,
1985
+ ): Promise<number> {
1986
+ const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
1987
+ const row = await getDbClient().get<{ c: number }>(
1988
+ `SELECT COUNT(*) AS c FROM (
1989
+ SELECT MIN(createdAt) AS firstAt FROM agent_tasks
1990
+ WHERE slackChannelId = ?
1991
+ AND slackThreadTs = ?
1992
+ AND creatorAgentId = ?
1993
+ AND source = 'mcp'
1994
+ AND (createdAt > ? OR taskType = 'script-run-step')
1995
+ GROUP BY CASE
1996
+ WHEN taskType = 'script-run-step' AND contextKey LIKE 'script-run:%:%'
1997
+ THEN substr(contextKey, 1, instr(substr(contextKey, 12), ':') + 10)
1998
+ ELSE id
1999
+ END
2000
+ ) WHERE firstAt > ?`,
2001
+ [channelId, threadTs, creatorAgentId, since, since],
2002
+ );
2003
+ return row?.c ?? 0;
2004
+ }
2005
+
1887
2006
  /**
1888
2007
  * Find the most recent CANCELLED task in a Slack thread. Used by the
1889
2008
  * follow-up re-delegation guard so a cancellation (worker SIGTERM,
@@ -468,9 +468,14 @@ class BunSqliteClient implements DbClient {
468
468
  if (!willRetry) warnIfSlowStatement(sql, ms);
469
469
  }
470
470
  };
471
+ // The first call opens the database and applies migrations; resolve it
472
+ // before the timer, so boot work is not logged as this statement's cost.
473
+ // Outside a transaction it resolves inside the retry loop, so a boot that
474
+ // hits SQLITE_BUSY boots again.
471
475
  const ctx = txContext.getStore();
472
476
  if (ctx && !ctx.closed) {
473
- return timed(() => op(this.getDatabase()));
477
+ const db = this.getDatabase();
478
+ return timed(() => op(db));
474
479
  }
475
480
  let sleptMs = 0;
476
481
  for (let attemptIndex = 0; ; attemptIndex++) {
@@ -482,10 +487,8 @@ class BunSqliteClient implements DbClient {
482
487
  // A top-level single statement is atomic, so a repeat on BUSY is
483
488
  // exact. The final attempt spins `finalAttemptSpinMs` (see
484
489
  // transaction()).
485
- return timed(
486
- () => this.shortSpin(this.getDatabase(), () => op(this.getDatabase()), !canRetryAfter),
487
- canRetryAfter,
488
- );
490
+ const db = this.getDatabase();
491
+ return timed(() => this.shortSpin(db, () => op(db), !canRetryAfter), canRetryAfter);
489
492
  } catch (err) {
490
493
  if (!isSqliteBusy(err) || !canRetryAfter || next === undefined) throw err;
491
494
  backoff = next;