@desplega.ai/agent-swarm 2.7.4 → 2.7.5

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 (253) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-g3ads55s.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.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-0wyynpn1.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
  16. package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
  17. package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
  18. package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
  19. package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
  20. package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
  21. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  22. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  23. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  24. package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
  25. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  26. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  27. package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
  28. package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
  29. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  30. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  31. package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
  32. package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
  33. package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
  34. package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
  35. package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
  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-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
  39. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  40. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  41. package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
  42. package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
  43. package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
  44. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  45. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
  47. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  48. package/dist/cli-fw2w0kva.js +47 -0
  49. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  50. package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
  51. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  52. package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
  53. package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
  54. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  55. package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
  56. package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
  57. package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
  58. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  59. package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
  60. package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
  61. package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
  62. package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
  63. package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
  64. package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
  65. package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
  66. package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
  67. package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
  68. package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
  69. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  70. package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
  71. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  72. package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
  73. package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
  74. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  75. package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
  76. package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
  77. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  78. package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
  79. package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
  80. package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
  81. package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
  82. package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
  83. package/dist/cli-zh3fp8v2.js +151 -0
  84. package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
  85. package/dist/cli.js +18 -18
  86. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
  87. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  88. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  89. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
  90. package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
  91. package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
  92. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
  93. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
  94. package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
  95. package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
  96. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  97. package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
  98. package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
  99. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
  100. package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
  101. package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
  102. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
  103. package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
  104. package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
  105. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  106. package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
  107. package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
  108. package/dist/fx-adapter-gt6pncj2.js +18 -0
  109. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
  110. package/dist/handlers-vwmba1m2.js +31 -0
  111. package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
  112. package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
  113. package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
  114. package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
  115. package/dist/index-5r71rf98.js +32 -0
  116. package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
  117. package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
  118. package/dist/index-anmwxs87.js +29 -0
  119. package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
  120. package/dist/index-spp0ra5c.js +36 -0
  121. package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
  122. package/dist/lead-fjy418cw.js +69 -0
  123. package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
  125. package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
  126. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
  127. package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
  128. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
  129. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
  130. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
  131. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
  132. package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
  133. package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
  134. package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
  135. package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
  136. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
  137. package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
  138. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
  139. package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
  140. package/dist/worker-0c5tyzqe.js +69 -0
  141. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
  142. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  143. package/openapi.json +97 -32
  144. package/package.json +1 -1
  145. package/src/be/db/session-summaries.ts +8 -2
  146. package/src/be/db/session-tree.ts +174 -61
  147. package/src/be/db/session-visibility.ts +5 -4
  148. package/src/be/db.ts +8 -4
  149. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  150. package/src/be/model-validation.ts +5 -1
  151. package/src/be/pricing-normalize.ts +7 -2
  152. package/src/be/scripts/typecheck.ts +1 -0
  153. package/src/be/seed/schedules-seeder.ts +135 -9
  154. package/src/be/seed/workflows-seeder.ts +65 -1
  155. package/src/be/seed-pricing.ts +23 -0
  156. package/src/be/steering.ts +50 -3
  157. package/src/be/swarm-config-guard.ts +1 -0
  158. package/src/commands/provider-credentials.ts +29 -2
  159. package/src/commands/runner.ts +86 -5
  160. package/src/extensions/contract-types.generated.ts +1 -1
  161. package/src/extensions/contract.ts +27 -1
  162. package/src/extensions/dispatcher.ts +4 -0
  163. package/src/http/agents.ts +1 -0
  164. package/src/http/mcp-bridge.ts +4 -0
  165. package/src/integrations/chat-sdk/inbound.ts +15 -5
  166. package/src/integrations/chat-sdk/steering.ts +3 -1
  167. package/src/kv-reserved-namespaces.ts +2 -0
  168. package/src/prompts/base-prompt.ts +4 -1
  169. package/src/prompts/session-templates.ts +26 -0
  170. package/src/providers/acp-adapter.ts +190 -26
  171. package/src/providers/acp-targets.ts +306 -6
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +9 -44
  174. package/src/providers/codex-oauth/flow.ts +24 -3
  175. package/src/providers/codex-oauth/storage.ts +56 -17
  176. package/src/providers/codex-oauth/subscription.ts +67 -0
  177. package/src/providers/fx-adapter.ts +211 -0
  178. package/src/providers/index.ts +5 -1
  179. package/src/providers/mcp-loopback-proxy.ts +94 -0
  180. package/src/providers/pi-mono-adapter.ts +98 -13
  181. package/src/providers/pricing-sources.md +8 -0
  182. package/src/providers/reasoning-effort.ts +17 -1
  183. package/src/providers/types.ts +2 -1
  184. package/src/scheduler/scheduler.ts +36 -3
  185. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  186. package/src/scripts-runtime/types/stdlib.d.ts +5 -0
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
  189. package/src/server.ts +7 -0
  190. package/src/slack/assistant.ts +17 -1
  191. package/src/slack/handlers.ts +18 -5
  192. package/src/slack/left-threads.ts +65 -0
  193. package/src/slack/steering.ts +3 -0
  194. package/src/slack/thread-buffer.ts +40 -16
  195. package/src/tests/acp-adapter.test.ts +79 -0
  196. package/src/tests/base-prompt.test.ts +12 -0
  197. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  198. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  199. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  200. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  201. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  202. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  203. package/src/tests/context-window.test.ts +7 -0
  204. package/src/tests/explicit-model-validation.test.ts +99 -0
  205. package/src/tests/extensions-run-context.test.ts +11 -0
  206. package/src/tests/extensions-steering.test.ts +353 -0
  207. package/src/tests/extensions-validate.test.ts +5 -0
  208. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  209. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  210. package/src/tests/fixtures/fake-fx.ts +167 -0
  211. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  212. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  213. package/src/tests/fx-adapter.test.ts +771 -0
  214. package/src/tests/grok-adapter.test.ts +36 -0
  215. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  216. package/src/tests/model-tier-resolution.test.ts +3 -3
  217. package/src/tests/pricing-refresh.test.ts +5 -5
  218. package/src/tests/prompt-template-session.test.ts +5 -3
  219. package/src/tests/provider-registration.test.ts +2 -1
  220. package/src/tests/query-audit-plans.test.ts +3 -2
  221. package/src/tests/script-connections.test.ts +2 -0
  222. package/src/tests/seed-model-profiles.test.ts +2 -2
  223. package/src/tests/seed-schedules.test.ts +138 -0
  224. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  225. package/src/tests/session-costs-golden.test.ts +26 -0
  226. package/src/tests/session-summaries-plans.test.ts +120 -0
  227. package/src/tests/session-summaries.test.ts +72 -12
  228. package/src/tests/sessions-tree.test.ts +11 -3
  229. package/src/tests/slack-leave-thread.test.ts +431 -0
  230. package/src/tests/tool-annotations.test.ts +2 -1
  231. package/src/tests/workflow-engine-v2.test.ts +42 -0
  232. package/src/tests/workflow-script-principal.test.ts +207 -7
  233. package/src/tools/slack-leave-thread.ts +104 -0
  234. package/src/tools/steer-task.ts +3 -2
  235. package/src/tools/tool-config.ts +2 -1
  236. package/src/types.ts +19 -0
  237. package/src/utils/codex-auth-failure.ts +30 -0
  238. package/src/utils/context-window.ts +4 -1
  239. package/src/utils/credentials.ts +7 -0
  240. package/src/utils/fx-models.ts +43 -0
  241. package/src/workflows/cooldown.ts +3 -2
  242. package/src/workflows/engine.ts +24 -9
  243. package/templates/schedules/dreaming-daily/config.json +1 -1
  244. package/templates/schedules/dreaming-daily/content.md +6 -14
  245. package/templates/skills/swarm-docs/content.md +1 -0
  246. package/templates/workflows/dreaming/content.md +5 -1
  247. package/dist/acp-adapter-96dc64m2.js +0 -15
  248. package/dist/handlers-jg7v74ty.js +0 -31
  249. package/dist/index-8c1zbz59.js +0 -29
  250. package/dist/index-8k8tmfxd.js +0 -32
  251. package/dist/index-hq617xdj.js +0 -36
  252. package/dist/lead-567kw7x3.js +0 -68
  253. package/dist/worker-fjdqra6q.js +0 -68
@@ -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");