@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
@@ -62,9 +62,9 @@ beforeEach(async () => {
62
62
 
63
63
  describe("tier seeds", () => {
64
64
  test("one profile per harness and tier, copied from the tier map; none for acp", async () => {
65
- expect(await seedModelProfiles()).toBe(40);
65
+ expect(await seedModelProfiles()).toBe(44);
66
66
  const profiles = await listModelProfiles();
67
- expect(profiles).toHaveLength(40);
67
+ expect(profiles).toHaveLength(44);
68
68
  expect(profiles.some((p) => p.harness === "acp")).toBe(false);
69
69
  for (const profile of profiles) {
70
70
  const tier = profile.seededFrom?.replace("tier:", "") as (typeof MODEL_TIERS)[number];
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import {
4
4
  closeDb,
5
+ createWorkflow,
5
6
  getScheduledTaskByName,
6
7
  getScheduledTasks,
7
8
  initDb,
@@ -13,6 +14,7 @@ import {
13
14
  loadSeedSchedules,
14
15
  type ScheduleTemplateSource,
15
16
  } from "../be/seed/schedules-seeder";
17
+ import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
16
18
 
17
19
  const TEST_DB_PATH = "./test-seed-schedules.sqlite";
18
20
 
@@ -72,6 +74,31 @@ function zeroConfigScheduleSource(opts?: {
72
74
  } satisfies ScheduleTemplateSource;
73
75
  }
74
76
 
77
+ /** A schedule template with an explicit target block and an optional task prompt. */
78
+ function targetScheduleSource(target: Record<string, unknown>, prompt?: string) {
79
+ return {
80
+ config: JSON.stringify({
81
+ name: "test-target-schedule",
82
+ title: "Test target schedule",
83
+ description: "Seeded schedule with a target.",
84
+ placeholders: [],
85
+ requires: [],
86
+ runAllSeedersCandidate: true,
87
+ autoEnableCandidate: true,
88
+ }),
89
+ content: `# Test\n\n## Schedule\n\n\`\`\`json\n${JSON.stringify({
90
+ cron: "0 9 * * *",
91
+ timezone: "UTC",
92
+ enabled: true,
93
+ ...target,
94
+ })}\n\`\`\`\n${prompt === undefined ? "" : `\n## Scheduled Task\n\n${prompt}`}`,
95
+ } satisfies ScheduleTemplateSource;
96
+ }
97
+
98
+ async function makeWorkflow(name: string) {
99
+ return createWorkflow({ name, definition: { nodes: [] } });
100
+ }
101
+
75
102
  const ORIGINAL_SEED_AUTOMATIONS_ENABLED = process.env.SEED_AUTOMATIONS_ENABLED;
76
103
 
77
104
  beforeEach(async () => {
@@ -133,6 +160,8 @@ describe("schedules seeder", () => {
133
160
 
134
161
  test("SEED_AUTOMATIONS_ENABLED=false still seeds all schedules disabled", async () => {
135
162
  process.env.SEED_AUTOMATIONS_ENABLED = "false";
163
+ // dreaming-daily targets the seeded dreaming workflow.
164
+ await runSeeder(createWorkflowsSeeder(), { quiet: true });
136
165
  const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
137
166
  expect(result).toMatchObject({ created: 12, failed: [] });
138
167
  const schedules = await getScheduledTasks();
@@ -141,6 +170,8 @@ describe("schedules seeder", () => {
141
170
  });
142
171
 
143
172
  test("seeds a disabled schedule whose timezone still needs setup", async () => {
173
+ // dreaming-daily targets the seeded dreaming workflow.
174
+ await runSeeder(createWorkflowsSeeder(), { quiet: true });
144
175
  const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
145
176
  expect(result).toMatchObject({ created: 12, failed: [] });
146
177
  expect(await getScheduledTaskByName("weekly-dependabot-triage")).toMatchObject({
@@ -245,6 +276,113 @@ describe("schedules seeder", () => {
245
276
  );
246
277
  });
247
278
 
279
+ test("dreaming-daily is a workflow schedule with no task prompt", async () => {
280
+ await runSeeder(createWorkflowsSeeder(), { quiet: true });
281
+ const dreaming = loadSeedSchedules().find((schedule) => schedule.name === "dreaming-daily");
282
+ expect(dreaming).toMatchObject({
283
+ targetType: "workflow",
284
+ workflow: "dreaming",
285
+ taskTemplate: "",
286
+ });
287
+ // Agent-task templates carry no target keys, so their seed hash is unchanged.
288
+ const digest = loadSeedSchedules().find((schedule) => schedule.name === "daily-blocker-digest");
289
+ expect(digest && "targetType" in digest).toBe(false);
290
+ });
291
+
292
+ test("seeds a workflow target by name and re-runs as a no-op", async () => {
293
+ const workflow = await makeWorkflow("test-target-workflow");
294
+ const seeder = createSchedulesSeeder([
295
+ targetScheduleSource({ targetType: "workflow", workflow: "test-target-workflow" }),
296
+ ]);
297
+ expect(await runSeeder(seeder, { quiet: true })).toMatchObject({ created: 1, failed: [] });
298
+ const row = await getScheduledTaskByName("test-target-schedule");
299
+ expect(row).toMatchObject({ targetType: "workflow", workflowId: workflow.id });
300
+ expect(row?.taskTemplate).toBeUndefined();
301
+ expect(await runSeeder(seeder, { quiet: true })).toMatchObject({
302
+ skippedUnchanged: 1,
303
+ updated: 0,
304
+ });
305
+ });
306
+
307
+ test("seeds a script target and fails on a missing script", async () => {
308
+ const seeder = createSchedulesSeeder([
309
+ targetScheduleSource({ targetType: "script", scriptName: "no-such-script", scriptArgs: {} }),
310
+ ]);
311
+ const result = await runSeeder(seeder, { quiet: true });
312
+ expect(result.created).toBe(0);
313
+ expect(result.failed[0]?.error).toContain('Global script "no-such-script" not found');
314
+ });
315
+
316
+ test("fails a workflow target whose workflow does not exist, without creating a row", async () => {
317
+ const result = await runSeeder(
318
+ createSchedulesSeeder([
319
+ targetScheduleSource({ targetType: "workflow", workflow: "missing" }),
320
+ ]),
321
+ { quiet: true },
322
+ );
323
+ expect(result.created).toBe(0);
324
+ expect(result.failed[0]?.error).toContain('Workflow "missing" not found');
325
+ expect(await getScheduledTaskByName("test-target-schedule")).toBeNull();
326
+ });
327
+
328
+ test("rejects target blocks that ship fields or prompts the target ignores", () => {
329
+ const cases: [Record<string, unknown>, string | undefined, string][] = [
330
+ [{ targetType: "workflow", workflow: "w" }, "Do it.", "ships a ## Scheduled Task prompt"],
331
+ [{ targetType: "script", scriptName: "s" }, "Do it.", "ships a ## Scheduled Task prompt"],
332
+ [{ targetType: "workflow" }, undefined, "has no workflow name"],
333
+ [{ targetType: "script" }, undefined, "has no scriptName"],
334
+ [{ targetType: "workflow", workflow: "w", scriptName: "s" }, undefined, "sets scriptName"],
335
+ [{ workflow: "w" }, "Do it.", "(targetType agent-task) sets workflow"],
336
+ [{ targetType: "lambda" }, undefined, 'unknown targetType "lambda"'],
337
+ ];
338
+ for (const [target, prompt, message] of cases) {
339
+ expect(() => loadSeedSchedules([targetScheduleSource(target, prompt)])).toThrow(message);
340
+ }
341
+ });
342
+
343
+ test("migrates a pristine agent-task schedule to a workflow target and clears its prompt", async () => {
344
+ const workflow = await makeWorkflow("test-target-workflow");
345
+ await runSeeder(createSchedulesSeeder([targetScheduleSource({}, "Trigger the workflow.")]), {
346
+ quiet: true,
347
+ });
348
+ expect(await getScheduledTaskByName("test-target-schedule")).toMatchObject({
349
+ targetType: "agent-task",
350
+ taskTemplate: "Trigger the workflow.",
351
+ });
352
+
353
+ const result = await runSeeder(
354
+ createSchedulesSeeder([
355
+ targetScheduleSource({ targetType: "workflow", workflow: "test-target-workflow" }),
356
+ ]),
357
+ { quiet: true },
358
+ );
359
+ expect(result).toMatchObject({ updated: 1, failed: [] });
360
+ const row = await getScheduledTaskByName("test-target-schedule");
361
+ expect(row).toMatchObject({ targetType: "workflow", workflowId: workflow.id });
362
+ expect(row?.taskTemplate).toBeUndefined();
363
+ });
364
+
365
+ test("preserves an operator-edited agent-task schedule when its template moves to a workflow", async () => {
366
+ await makeWorkflow("test-target-workflow");
367
+ await runSeeder(createSchedulesSeeder([targetScheduleSource({}, "Trigger the workflow.")]), {
368
+ quiet: true,
369
+ });
370
+ const seeded = await getScheduledTaskByName("test-target-schedule");
371
+ await updateScheduledTask(seeded!.id, { taskTemplate: "My own prompt." });
372
+
373
+ const result = await runSeeder(
374
+ createSchedulesSeeder([
375
+ targetScheduleSource({ targetType: "workflow", workflow: "test-target-workflow" }),
376
+ ]),
377
+ { quiet: true },
378
+ );
379
+ expect(result.skippedUserModified).toBe(1);
380
+ expect(await getScheduledTaskByName("test-target-schedule")).toMatchObject({
381
+ targetType: "agent-task",
382
+ taskTemplate: "My own prompt.",
383
+ });
384
+ });
385
+
248
386
  test("reports an invalid cron without creating a row", async () => {
249
387
  const result = await runSeeder(createSchedulesSeeder([scheduleSource("not a cron")]), {
250
388
  quiet: true,
@@ -217,6 +217,32 @@ describe("seed-scripts catalog", () => {
217
217
  ).toHaveLength(200);
218
218
  });
219
219
 
220
+ test.each([
221
+ ["Incomplete response returned, reason: max_output_tokens", "output-limit"],
222
+ ["Stopped: max_tokens", "output-limit"],
223
+ ["Maximum output length exceeded", "output-limit"],
224
+ ["Output token limit reached", "output-limit"],
225
+ ["401 Unauthorized", "auth/credentials"],
226
+ ["invalid token", "auth/credentials"],
227
+ ["Resource not accessible by personal access token", "auth/credentials"],
228
+ ["Processed 100 tokens", "other"],
229
+ ["max_input_tokens exceeded", "other"],
230
+ ])("task-failure-audit classifies %s as %s", async (failureReason, expectedKey) => {
231
+ const result = await taskFailureAudit(
232
+ { publishPage: false },
233
+ {
234
+ swarm: {
235
+ async task_list() {
236
+ return { data: { tasks: [{ id: "failed-task", failureReason }] } };
237
+ },
238
+ },
239
+ },
240
+ );
241
+
242
+ expect(result.groups).toHaveLength(1);
243
+ expect(result.groups[0].key).toBe(expectedKey);
244
+ });
245
+
220
246
  test("task-failure-audit projects failure reasons into schedule-group samples", async () => {
221
247
  let projectionArgs: Record<string, unknown> | undefined;
222
248
  const result = await taskFailureAudit(
@@ -261,3 +261,42 @@ describe("nan and helmcode sessions price at $0 from the seeded table", () => {
261
261
  }
262
262
  });
263
263
  });
264
+
265
+ describe("pi on the ChatGPT subscription prices like codex on the same pool", () => {
266
+ const cache = loadModelsDevCache();
267
+ const rows = cache ? buildModelsDevSeedRows(cache) : [];
268
+ const book: RateBook = Object.fromEntries(
269
+ rows.map((row) => [`${row.provider}:${row.model}:${row.tokenClass}`, row.pricePerMillionUsd]),
270
+ );
271
+
272
+ test("openai-codex/<id> on pi records the codex <id> pricing-table cost", async () => {
273
+ // One request: 69,308 input tokens, 55,680 of them cached, 376 output.
274
+ // codex reports input including cache reads; pi-ai reports them disjoint.
275
+ const codex = await recomputeSessionCost(
276
+ {
277
+ ...BASE,
278
+ provider: "codex",
279
+ model: "gpt-5.6-luna",
280
+ inputTokens: 69_308,
281
+ cacheReadTokens: 55_680,
282
+ outputTokens: 376,
283
+ },
284
+ lookupFrom(book),
285
+ );
286
+ const pi = await recomputeSessionCost(
287
+ {
288
+ ...BASE,
289
+ provider: "pi",
290
+ model: "openai-codex/gpt-5.6-luna",
291
+ inputTokens: 69_308 - 55_680,
292
+ cacheReadTokens: 55_680,
293
+ outputTokens: 376,
294
+ },
295
+ lookupFrom(book),
296
+ );
297
+ expect(codex.costSource).toBe("pricing-table");
298
+ expect(pi.costSource).toBe("pricing-table");
299
+ expect(pi.totalCostUsd).toBeCloseTo(codex.totalCostUsd, 12);
300
+ expect(pi.totalCostUsd).toBeGreaterThan(0);
301
+ });
302
+ });
@@ -361,6 +361,32 @@ describe("Phase 3 — session cost recompute golden fixtures", () => {
361
361
  expectExact(body.cost.totalCostUsd, expected);
362
362
  });
363
363
 
364
+ // Boot-seeded rows only: fx reports the swarm model string with its route
365
+ // prefix and no cache split, so input and output price at the table rate.
366
+ test("fx runs price from the seeded rates on each route", async () => {
367
+ const openRouter = await postCost({
368
+ sessionId: "fx-openrouter",
369
+ provider: "fx",
370
+ model: "openrouter/deepseek/deepseek-v4.1-flash",
371
+ totalCostUsd: 0,
372
+ inputTokens: 58_620,
373
+ outputTokens: 1_183,
374
+ });
375
+ expect(openRouter.cost.costSource).toBe("pricing-table");
376
+ expectExact(openRouter.cost.totalCostUsd, (58_620 * 0.14 + 1_183 * 0.42) / 1_000_000);
377
+
378
+ const openAi = await postCost({
379
+ sessionId: "fx-openai",
380
+ provider: "fx",
381
+ model: "gpt-5-mini",
382
+ totalCostUsd: 0,
383
+ inputTokens: 9_286,
384
+ outputTokens: 506,
385
+ });
386
+ expect(openAi.cost.costSource).toBe("pricing-table");
387
+ expectExact(openAi.cost.totalCostUsd, (9_286 * 0.25 + 506 * 2) / 1_000_000);
388
+ });
389
+
364
390
  test("legacy cache writes without a TTL split keep the 5m class", async () => {
365
391
  await seedModelRates("claude", "claude-legacy-cache-write", {
366
392
  input: 5,
@@ -0,0 +1,120 @@
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ countSessions,
6
+ createTaskExtended,
7
+ createUser,
8
+ getDbClient,
9
+ initDb,
10
+ listRecentSessions,
11
+ } from "../be/db";
12
+ import {
13
+ getSessionReadState,
14
+ getSessionTree,
15
+ legacySessionSummaryQuery,
16
+ triageSessionList,
17
+ } from "../be/db/session-tree";
18
+
19
+ const TEST_DB_PATH = "./test-session-summaries-plans.sqlite";
20
+
21
+ async function removeDbFiles(): Promise<void> {
22
+ for (const suffix of ["", "-wal", "-shm"])
23
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
24
+ }
25
+
26
+ // Runs the real caller and returns the plans of every statement it sent.
27
+ async function plansOf(run: () => Promise<unknown>): Promise<string[]> {
28
+ const client = getDbClient();
29
+ const seen: { sql: string; params: unknown[] }[] = [];
30
+ const methods = ["query", "get", "run"] as const;
31
+ const originals = methods.map((name) => client[name].bind(client));
32
+ const spies = methods.map((name, i) =>
33
+ spyOn(client, name).mockImplementation(((sql: string, params?: unknown[]) => {
34
+ seen.push({ sql, params: params ?? [] });
35
+ return (originals[i] as (sql: string, params?: unknown[]) => unknown)(sql, params);
36
+ }) as never),
37
+ );
38
+ try {
39
+ await run();
40
+ } finally {
41
+ for (const spy of spies) spy.mockRestore();
42
+ }
43
+ expect(seen.length).toBeGreaterThan(0);
44
+ const plans: string[] = [];
45
+ for (const { sql, params } of seen) {
46
+ const rows = await client.query<{ detail: string }>(
47
+ `EXPLAIN QUERY PLAN ${sql}`,
48
+ params as (string | number | null)[],
49
+ );
50
+ plans.push(rows.map((row) => row.detail).join("\n"));
51
+ }
52
+ return plans;
53
+ }
54
+
55
+ let userId = "";
56
+
57
+ describe("session read plans without sqlite_stat1", () => {
58
+ beforeAll(async () => {
59
+ await removeDbFiles();
60
+ initDb(TEST_DB_PATH);
61
+ userId = (await createUser({ name: "plans viewer" })).id;
62
+ const root = await createTaskExtended("plans root", {
63
+ source: "ui",
64
+ requestedByUserId: userId,
65
+ });
66
+ await createTaskExtended("plans child", { parentTaskId: root.id });
67
+ });
68
+
69
+ afterAll(async () => {
70
+ closeDb();
71
+ await removeDbFiles();
72
+ });
73
+
74
+ test("the test DB has no planner statistics, like production", async () => {
75
+ const stat = await getDbClient().get<{ name: string }>(
76
+ "SELECT name FROM sqlite_master WHERE name = 'sqlite_stat1'",
77
+ );
78
+ expect(stat).toBeNull();
79
+ });
80
+
81
+ const viewer = () => ({ userId, scope: "all" as const });
82
+ const now = "2026-10-29T12:00:00.000Z";
83
+ const CASES: [string, () => Promise<unknown>][] = [
84
+ ["list", () => listRecentSessions({ viewer: viewer(), now, slim: true, tree: true })],
85
+ ["list inbox", () => listRecentSessions({ viewer: viewer(), now, triage: "inbox" })],
86
+ ["list done", () => listRecentSessions({ viewer: viewer(), now, triage: "done" })],
87
+ ["list group", () => listRecentSessions({ viewer: viewer(), now, groupKey: "shared/" })],
88
+ ["list status", () => listRecentSessions({ viewer: viewer(), now, status: "needs" })],
89
+ ["list with user", () => listRecentSessions({ viewer: viewer(), now, withUserId: userId })],
90
+ ["count", () => countSessions({ viewer: viewer(), now, tree: true })],
91
+ ["count inbox", () => countSessions({ viewer: viewer(), now, triage: "inbox" })],
92
+ ["count source", () => countSessions({ source: ["slack"] })],
93
+ ["tree", () => getSessionTree({ kind: "user", userId }, now)],
94
+ ["tree mine", () => getSessionTree({ kind: "user", userId }, now, { mine: true })],
95
+ ["read state", () => getSessionReadState(userId, userId, now)],
96
+ ["triage list", () => triageSessionList(userId, { triage: "inbox", now }, "read")],
97
+ ];
98
+
99
+ for (const [name, run] of CASES) {
100
+ test(`${name} reads the summaries without a chain walk or task scan`, async () => {
101
+ for (const detail of await plansOf(run)) {
102
+ expect(detail).not.toContain("RECURSIVE");
103
+ expect(detail).not.toMatch(/\bSCAN agent_tasks\b/);
104
+ expect(detail).not.toMatch(/\bSCAN (r|rt|t)\b/);
105
+ }
106
+ });
107
+ }
108
+
109
+ test("the chain walk trips the same checks (control)", async () => {
110
+ const query = legacySessionSummaryQuery({ viewer: viewer(), now });
111
+ const [detail] = await plansOf(() => getDbClient().query(query.sql, query.params));
112
+ expect(detail).toMatch(/RECURSIVE|\bSCAN agent_tasks\b|\bSCAN (r|t)\b/);
113
+ });
114
+
115
+ test("the list reads agent_tasks only for the page roots", async () => {
116
+ const [detail] = await plansOf(() => listRecentSessions({ viewer: viewer(), now, slim: true }));
117
+ expect(detail).toContain("SEARCH r USING INDEX sqlite_autoindex_agent_tasks_1 (id=?)");
118
+ expect(detail).toContain("SCAN session_summaries USING INDEX idx_session_summaries_activity");
119
+ });
120
+ });
@@ -13,11 +13,17 @@ import {
13
13
  upsertInboxState,
14
14
  } from "../be/db";
15
15
  import { findSessionSummaryDrift } from "../be/db/session-summaries";
16
- import { sessionSummaryQuery } from "../be/db/session-tree";
16
+ import {
17
+ legacySessionSummaryQuery,
18
+ type SessionFilters,
19
+ sessionSummaryQuery,
20
+ } from "../be/db/session-tree";
17
21
  import { deleteTask } from "../be/db/tasks/write";
18
22
 
19
23
  const dbPath = join(tmpdir(), `session-summaries-${process.pid}-${Date.now()}.sqlite`);
20
- const MIGRATION = join(import.meta.dir, "../be/migrations/253_session_summaries.sql");
24
+ const MIGRATIONS = ["253_session_summaries.sql", "259_session_summaries_open_tasks.sql"].map(
25
+ (name) => join(import.meta.dir, "../be/migrations", name),
26
+ );
21
27
  const BASE = Date.parse("2026-10-01T00:00:00.000Z");
22
28
  let users: string[] = [];
23
29
 
@@ -209,7 +215,7 @@ const SCHEDULE = [
209
215
  /** Each Done-marked root's read state from the chain walk equals the stored rule. */
210
216
  async function expectReopensDoneMatches() {
211
217
  const viewer = users[0]!;
212
- const legacy = sessionSummaryQuery({ viewer: { userId: viewer, scope: "all" } });
218
+ const legacy = legacySessionSummaryQuery({ viewer: { userId: viewer, scope: "all" } });
213
219
  const rows = await client().query<{ root_id: string; readState: string; stored: string }>(
214
220
  `SELECT a.root_id, a.readState,
215
221
  CASE WHEN s.lastHumanAt > d.lastUpdatedAt THEN 'unread' ELSE 'done' END AS stored
@@ -226,6 +232,49 @@ async function expectReopensDoneMatches() {
226
232
  return rows.length;
227
233
  }
228
234
 
235
+ /** A random filter set, as the list, count and tree send them. */
236
+ function randomFilters(): SessionFilters {
237
+ const opts: SessionFilters = {
238
+ viewer: { userId: rand() < 0.15 ? null : pick(users), scope: pick(["all", "mine", "shared"]) },
239
+ now: at(),
240
+ };
241
+ if (rand() < 0.2) opts.source = [pick(["ui", "slack", "mcp", "api"])];
242
+ if (rand() < 0.2) opts.showSource = [pick(["ui", "slack", "api"])];
243
+ if (rand() < 0.2) opts.hideSource = [pick(["mcp", "system"])];
244
+ if (rand() < 0.4) opts.triage = pick(["inbox", "later", "done", "unread"] as const);
245
+ if (rand() < 0.3) opts.status = pick(["running", "failed", "needs"] as const);
246
+ if (rand() < 0.2) opts.withUserId = pick(users);
247
+ if (rand() < 0.2) opts.q = pick(["task", "ab", "zz"]);
248
+ if (rand() < 0.2) opts.noGroup = true;
249
+ else if (rand() < 0.1) opts.groupKey = "shared/";
250
+ if (rand() < 0.3) opts.tree = true;
251
+ if (rand() < 0.2) opts.mine = true;
252
+ return opts;
253
+ }
254
+
255
+ /** The summary query returns the chain walk's rows for random filters. */
256
+ async function expectSummaryQueryMatchesLegacy(sets: number) {
257
+ const columns = `root_id, chainCount, lastActivityAt, latestStatus, readState, snoozeUntil,
258
+ needsYou, pendingApproval > 0 AS pendingApproval, openTasks`;
259
+ // Skip roots whose newest timestamp ties across chain tasks: the walk's status
260
+ // is arbitrary there, and so are needsYou and the status filters.
261
+ const untied = `(SELECT COUNT(*) FROM session_members m JOIN agent_tasks t ON t.id = m.taskId
262
+ WHERE m.rootId = q.root_id AND t.lastUpdatedAt = q.lastActivityAt) = 1`;
263
+ let rows = 0;
264
+ for (let i = 0; i < sets; i++) {
265
+ const opts = randomFilters();
266
+ const read = async (query: { sql: string; params: unknown[] }) =>
267
+ await client().query<Record<string, unknown>>(
268
+ `SELECT ${columns} FROM (${query.sql}) q WHERE ${untied} ORDER BY root_id`,
269
+ query.params as (string | number | null)[],
270
+ );
271
+ const expected = await read(legacySessionSummaryQuery(opts));
272
+ expect({ opts, rows: await read(sessionSummaryQuery(opts)) }).toEqual({ opts, rows: expected });
273
+ rows += expected.length;
274
+ }
275
+ return rows;
276
+ }
277
+
229
278
  beforeAll(async () => {
230
279
  initDb(dbPath);
231
280
  users = [];
@@ -245,8 +294,9 @@ describe("session_summaries", () => {
245
294
  test("500 random writes keep the summaries equal to the chain walk", async () => {
246
295
  const counts: Record<string, number> = {};
247
296
  // The most chains, human events and pending approvals any checkpoint compared.
248
- const seen = { chains: 0, humans: 0, pending: 0 };
297
+ const seen = { chains: 0, humans: 0, pending: 0, open: 0 };
249
298
  let doneChecks = 0;
299
+ let summaryRows = 0;
250
300
  for (let i = 1; i <= 500; i++) {
251
301
  const op = pick(SCHEDULE);
252
302
  counts[op] = (counts[op] ?? 0) + 1;
@@ -257,9 +307,11 @@ describe("session_summaries", () => {
257
307
  drift: [],
258
308
  });
259
309
  doneChecks += await expectReopensDoneMatches();
310
+ summaryRows += await expectSummaryQueryMatchesLegacy(5);
260
311
  const shape = (await client().get<typeof seen>(
261
312
  `SELECT SUM(chainCount > 2) AS chains, SUM(lastHumanAt IS NOT NULL) AS humans,
262
- SUM(pendingApprovalCount > 0) AS pending FROM session_summaries`,
313
+ SUM(pendingApprovalCount > 0) AS pending, SUM(openTaskCount > 1) AS open
314
+ FROM session_summaries`,
263
315
  ))!;
264
316
  for (const key of Object.keys(seen) as (keyof typeof seen)[])
265
317
  seen[key] = Math.max(seen[key], shape[key] ?? 0);
@@ -270,7 +322,9 @@ describe("session_summaries", () => {
270
322
  expect(seen.chains).toBeGreaterThan(5);
271
323
  expect(seen.humans).toBeGreaterThan(5);
272
324
  expect(seen.pending).toBeGreaterThan(0);
325
+ expect(seen.open).toBeGreaterThan(0);
273
326
  expect(doneChecks).toBeGreaterThan(0);
327
+ expect(summaryRows).toBeGreaterThan(100);
274
328
  }, 60_000);
275
329
 
276
330
  test("the drift check reports a corrupted summary and member (positive control)", async () => {
@@ -279,13 +333,13 @@ describe("session_summaries", () => {
279
333
  expect(await findSessionSummaryDrift()).toEqual([]);
280
334
 
281
335
  await client().run(
282
- "UPDATE session_summaries SET chainCount = chainCount + 5, lastHumanAt = 'x' WHERE rootId = ?",
336
+ "UPDATE session_summaries SET chainCount = chainCount + 5, openTaskCount = 7, lastHumanAt = 'x' WHERE rootId = ?",
283
337
  [root],
284
338
  );
285
339
  await client().run("DELETE FROM session_members WHERE taskId = ?", [child]);
286
340
  const drift = await findSessionSummaryDrift();
287
341
  expect(drift.map((d) => `${d.rootId}:${d.field}`).sort()).toEqual(
288
- [`${root}:chainCount`, `${root}:lastHumanAt`, `${root}:member`].sort(),
342
+ [`${root}:chainCount`, `${root}:lastHumanAt`, `${root}:member`, `${root}:openTasks`].sort(),
289
343
  );
290
344
 
291
345
  await client().run("INSERT INTO session_members (taskId, rootId) VALUES (?, ?)", [child, root]);
@@ -356,18 +410,24 @@ describe("session_summaries", () => {
356
410
  await client().run("UPDATE agent_tasks SET parentTaskId = ? WHERE id = ?", [root, child]);
357
411
  });
358
412
 
359
- test("the migration backfill rebuilds the same summaries", async () => {
413
+ test("the migration backfills rebuild the same summaries", async () => {
360
414
  const before = await client().query<Record<string, unknown>>(
361
415
  "SELECT * FROM session_summaries ORDER BY rootId",
362
416
  );
363
417
  expect(before.length).toBeGreaterThan(20);
364
- const sql = await Bun.file(MIGRATION).text();
365
- const backfill = sql.slice(sql.indexOf("-- Backfill:"));
366
- expect(backfill).toContain("INSERT INTO session_members");
418
+ const backfills = await Promise.all(
419
+ MIGRATIONS.map(async (path) => {
420
+ const sql = await Bun.file(path).text();
421
+ return sql.slice(sql.indexOf("-- Backfill:"));
422
+ }),
423
+ );
424
+ expect(backfills[0]).toContain("INSERT INTO session_members");
425
+ expect(backfills[1]).toContain("openTaskCount");
367
426
  await client().transaction(async (tx) => {
368
427
  await tx.run("DELETE FROM session_summaries");
369
428
  await tx.run("DELETE FROM session_members");
370
- for (const statement of backfill
429
+ for (const statement of backfills
430
+ .join("\n")
371
431
  .split(/;\s*\n/)
372
432
  .filter((s) => s.replace(/--.*$/gm, "").trim()))
373
433
  await tx.run(statement);
@@ -425,14 +425,22 @@ describe("nested source thread groups", () => {
425
425
  }
426
426
  const [sql, params] = call;
427
427
  const plan = await client.query<{ detail: string }>(`EXPLAIN QUERY PLAN ${sql}`, params);
428
- expect(plan.some((p) => p.detail.startsWith("SEARCH r USING INDEX"))).toBe(true);
429
428
  expect(
430
429
  plan.some((p) =>
431
- p.detail.includes("SEARCH session_thread_root USING INDEX idx_agent_tasks_asset_key"),
430
+ /^SEARCH session_summaries USING INDEX idx_session_summaries_(key|thread) \(key>\? AND key<\?\)/.test(
431
+ p.detail,
432
+ ),
432
433
  ),
433
434
  ).toBe(true);
434
435
  expect(
435
- plan.some((p) => p.detail === "SCAN r" || p.detail === "SCAN session_thread_root"),
436
+ plan.some((p) =>
437
+ p.detail.includes("SEARCH session_thread_root USING INDEX idx_session_summaries_key"),
438
+ ),
439
+ ).toBe(true);
440
+ expect(
441
+ plan.some((p) =>
442
+ /^SCAN (session_summaries|session_thread_root|agent_tasks)\b/.test(p.detail),
443
+ ),
436
444
  ).toBe(false);
437
445
  expect(sql).not.toContain('"key" LIKE');
438
446
  expect(sql).not.toContain("INDEXED BY");