@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.
- package/README.md +1 -1
- package/dist/acp-adapter-bp91d37f.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
- package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
- package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
- package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
- package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
- package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
- package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
- package/dist/cli-54t00gt8.js +151 -0
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
- package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
- package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
- package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
- package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
- package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
- package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
- package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/cli-gdhpcbhf.js +55 -0
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
- package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
- package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
- package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
- package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
- package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
- package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
- package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
- package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
- package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
- package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
- package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
- package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
- package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
- package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
- package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
- package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
- package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
- package/dist/cli.js +26 -25
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
- package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
- package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
- package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
- package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
- package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
- package/dist/fx-adapter-fy8rr921.js +18 -0
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
- package/dist/handlers-by3sfxyx.js +32 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
- package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
- package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
- package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
- package/dist/index-4f2pvxae.js +37 -0
- package/dist/index-98gz7tmp.js +30 -0
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k08z2cak.js +33 -0
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
- package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
- package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
- package/dist/lead-enq35nqp.js +70 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
- package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
- package/dist/worker-gg1576j3.js +70 -0
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +189 -32
- package/package.json +8 -7
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db/tasks/read.ts +122 -3
- package/src/be/db-client.ts +8 -5
- package/src/be/db.ts +23 -8
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +23 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/index.ts +1 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/mcp-bridge.ts +4 -0
- package/src/http/memory.ts +38 -9
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/scheduler/scheduler.ts +36 -3
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +27 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
- package/src/server-user.ts +1 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +53 -5
- package/src/slack/left-threads.ts +88 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +8 -2
- package/src/tests/db-task-write-contract.test.ts +5 -1
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +113 -8
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +30 -2
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/script-connections.test.ts +13 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/seed-scripts.test.ts +26 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +571 -0
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/task-dedup.ts +45 -21
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +57 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- 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 `
|
|
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
|
|
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
|
-
/**
|
|
121
|
-
|
|
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: `(
|
|
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
|
-
|
|
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 = [
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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.
|
|
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
|
|
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
|
-
`
|
|
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
|
|
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 =
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
${
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
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 (
|
|
618
|
-
threadRootIds.push(row.
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
59
|
-
WHERE s.
|
|
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
|
}
|
package/src/be/db/tasks/read.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 = '
|
|
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,
|
package/src/be/db-client.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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;
|