@desplega.ai/agent-swarm 2.4.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
- package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
- package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
- package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
- package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
- package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
- package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
- package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
- package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +29 -27
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
- package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
- package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
- package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
- package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +785 -5
- package/package.json +9 -9
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/read.ts +40 -4
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +80 -236
- package/src/be/onboarding.ts +3 -0
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +2 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/index.ts +4 -0
- package/src/http/onboarding.ts +1 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +1 -0
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +19 -3
- package/src/integrations/chat-sdk/facts.ts +30 -12
- package/src/integrations/chat-sdk/outbound.ts +38 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +19 -28
- package/src/tasks/context-key.ts +61 -1
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-inbound.test.ts +39 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +2 -1
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +3 -2
- package/src/utils/config-env-export.ts +1 -0
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-9hstj09h.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-7gw64dxw.js +0 -39
- package/dist/index-bgdyvcnv.js +0 -40
- package/dist/index-gabbcdn2.js +0 -43
- package/dist/index-tfb26zhy.js +0 -41
- package/dist/lead-7e02z839.js +0 -77
- package/dist/worker-5snytpbd.js +0 -77
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-swarm.dev",
|
|
@@ -179,8 +179,8 @@
|
|
|
179
179
|
"@desplega/model-catalog": "workspace:*",
|
|
180
180
|
"@desplega/model-routing": "workspace:*",
|
|
181
181
|
"@faker-js/faker": "^10.4.0",
|
|
182
|
-
"@opencode/plugin": "2.1.
|
|
183
|
-
"@opencode/sdk": "2.1.
|
|
182
|
+
"@opencode/plugin": "2.1.3",
|
|
183
|
+
"@opencode/sdk": "2.1.3",
|
|
184
184
|
"@oxlint/plugins": "1.78.0",
|
|
185
185
|
"@types/bun": "latest",
|
|
186
186
|
"dependency-cruiser": "^18.0.0",
|
|
@@ -198,10 +198,10 @@
|
|
|
198
198
|
"bun": ">=1.3.12"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@agentclientprotocol/sdk": "2.0.
|
|
201
|
+
"@agentclientprotocol/sdk": "2.0.1",
|
|
202
202
|
"@ai-sdk/openai": "^3.0.41",
|
|
203
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
204
|
-
"@anthropic-ai/sdk": "^
|
|
203
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.308",
|
|
204
|
+
"@anthropic-ai/sdk": "^1.1.0",
|
|
205
205
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
206
206
|
"@aws-sdk/client-bedrock": "3.1048.0",
|
|
207
207
|
"@chat-adapter/discord": "4.41.1",
|
|
@@ -211,9 +211,9 @@
|
|
|
211
211
|
"@cursor/sdk": "1.0.40",
|
|
212
212
|
"@desplega.ai/agent-fs-just-bash": "0.18.5",
|
|
213
213
|
"@desplega.ai/business-use": "^0.4.2",
|
|
214
|
-
"@earendil-works/pi-agent-core": "^1.
|
|
215
|
-
"@earendil-works/pi-ai": "^1.
|
|
216
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
214
|
+
"@earendil-works/pi-agent-core": "^1.4.0",
|
|
215
|
+
"@earendil-works/pi-ai": "^1.4.0",
|
|
216
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
217
217
|
"@inkjs/ui": "^2.0.0",
|
|
218
218
|
"@jitl/quickjs-singlefile-mjs-release-sync": "0.32.0",
|
|
219
219
|
"@linear/sdk": "^77.0.0",
|
package/src/assets/key.ts
CHANGED
|
@@ -83,6 +83,23 @@ export function defaultAssetKey(resource: AssetKeyResource, id: string): string
|
|
|
83
83
|
return normalizeAssetKey(`shared/${resource}:${id}/`);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** Bounds for an indexed, literal prefix lookup on canonical asset keys. */
|
|
87
|
+
export function keyPrefixRange(prefix: string): { lo: string; hi: string } {
|
|
88
|
+
const lo = normalizeAssetKey(prefix);
|
|
89
|
+
return { lo, hi: `${lo.slice(0, -1)}${String.fromCharCode(lo.charCodeAt(lo.length - 1) + 1)}` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const SESSION_LEAF_KINDS = ["task", "thread", "schedule", "workflow"] as const;
|
|
93
|
+
const sessionLeafPattern = new RegExp(`/(?:${SESSION_LEAF_KINDS.join("|")}):[^/]+/$`);
|
|
94
|
+
|
|
95
|
+
/** Remove only a resource leaf, preserving user groups that contain a colon. */
|
|
96
|
+
export function sessionGroupKey(key: string): string {
|
|
97
|
+
const normalized = normalizeAssetKey(key);
|
|
98
|
+
const namespace = parseAssetKey(normalized);
|
|
99
|
+
if (namespace.relativeSegments.length === 0) return normalized;
|
|
100
|
+
return normalized.replace(sessionLeafPattern, "/");
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
export function parseAssetKey(input: string): AssetNamespace {
|
|
87
104
|
const key = normalizeAssetKey(input);
|
|
88
105
|
const segments = key.slice(0, -1).split("/");
|
|
@@ -394,7 +394,7 @@ export async function handlePullRequestCommented(
|
|
|
394
394
|
);
|
|
395
395
|
|
|
396
396
|
// Check if there's already an active task for this PR
|
|
397
|
-
const existingTask = await findTaskByVcs(repo, pr.pullRequestId);
|
|
397
|
+
const existingTask = await findTaskByVcs(repo, pr.pullRequestId, "azure-devops");
|
|
398
398
|
const existingTaskNote = existingTask
|
|
399
399
|
? `\n\n_Note: There's an active task (${existingTask.id}) for this PR #${pr.pullRequestId}._`
|
|
400
400
|
: "";
|
package/src/be/db/approvals.ts
CHANGED
|
@@ -144,3 +144,21 @@ export function scrubApprovalVotesJson(approvals: ApprovalVote[]): ScrubbedText
|
|
|
144
144
|
if (!breaksJsonValidity(raw, scrubbed)) return scrubbed;
|
|
145
145
|
return JSON.stringify(scrubObject(approvals)) as ScrubbedText;
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/** Pending approval summaries for an already loaded session chain. */
|
|
149
|
+
export async function getPendingApprovalsForTasks(taskIds: string[]): Promise<
|
|
150
|
+
{
|
|
151
|
+
id: string;
|
|
152
|
+
status: "pending";
|
|
153
|
+
sourceTaskId: string;
|
|
154
|
+
title: string;
|
|
155
|
+
}[]
|
|
156
|
+
> {
|
|
157
|
+
if (!taskIds.length) return [];
|
|
158
|
+
return getDbClient().query(
|
|
159
|
+
`SELECT id, status, sourceTaskId, title FROM approval_requests
|
|
160
|
+
WHERE sourceTaskId IN (${taskIds.map(() => "?").join(",")}) AND status = 'pending'
|
|
161
|
+
ORDER BY createdAt, id`,
|
|
162
|
+
taskIds,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { keyPrefixRange } from "../../assets/key";
|
|
2
|
+
import type { AssetEntityType } from "../../types";
|
|
3
|
+
import type { AssetKeyAuditResult } from "../asset-key-audit";
|
|
4
|
+
import { isInTransaction } from "../db-client";
|
|
5
|
+
import { getDbClient } from "./runtime";
|
|
6
|
+
import { sessionViewerCondition } from "./session-visibility";
|
|
7
|
+
import { getTaskById } from "./tasks/read";
|
|
8
|
+
|
|
9
|
+
type SessionMoveDependencies = {
|
|
10
|
+
/** The sync boot audit (`auditAssetKeys`) over the raw handle, which only db.ts may use. */
|
|
11
|
+
auditAssetKeys: () => AssetKeyAuditResult;
|
|
12
|
+
insertAssetKeyHistory: (input: {
|
|
13
|
+
entityType: AssetEntityType;
|
|
14
|
+
entityId: string;
|
|
15
|
+
previousKey?: string | null;
|
|
16
|
+
newKey: string;
|
|
17
|
+
changedBy?: string;
|
|
18
|
+
}) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let insertAssetKeyHistory: SessionMoveDependencies["insertAssetKeyHistory"];
|
|
22
|
+
let auditAssetKeys: SessionMoveDependencies["auditAssetKeys"];
|
|
23
|
+
|
|
24
|
+
export function configureSessionMoveDependencies(value: SessionMoveDependencies): void {
|
|
25
|
+
insertAssetKeyHistory = value.insertAssetKeyHistory;
|
|
26
|
+
auditAssetKeys = value.auditAssetKeys;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class AssetKeyMovesBlockedError extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(
|
|
32
|
+
"Asset namespace moves are blocked until structural errors, unknown personal users, or provider mapping drift are repaired.",
|
|
33
|
+
);
|
|
34
|
+
this.name = "AssetKeyMovesBlockedError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function assertAssetKeyMovesAllowed(): Promise<void> {
|
|
39
|
+
// auditAssetKeys stays sync (shared with the boot audit); run it inside a
|
|
40
|
+
// client transaction so this request-path read cannot observe another
|
|
41
|
+
// request's uncommitted writes on the shared connection.
|
|
42
|
+
const audit = await getDbClient().transaction(async () => auditAssetKeys(), {
|
|
43
|
+
readOnly: true,
|
|
44
|
+
});
|
|
45
|
+
if (!audit.ok) {
|
|
46
|
+
throw new AssetKeyMovesBlockedError();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Move only authorized task ids. The caller owns the transaction and audit gate. */
|
|
51
|
+
export async function moveTaskChainKeyInTx(
|
|
52
|
+
rootId: string,
|
|
53
|
+
taskIds: readonly string[],
|
|
54
|
+
key: string,
|
|
55
|
+
changedBy?: string,
|
|
56
|
+
): Promise<string[] | false> {
|
|
57
|
+
if (!isInTransaction()) {
|
|
58
|
+
throw new Error("moveTaskChainKeyInTx requires an active transaction");
|
|
59
|
+
}
|
|
60
|
+
const client = getDbClient();
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
const root = await getTaskById(rootId);
|
|
63
|
+
if (!root) return false;
|
|
64
|
+
const tasks = [root];
|
|
65
|
+
for (const id of taskIds) {
|
|
66
|
+
const task = id === root.id ? null : await getTaskById(id);
|
|
67
|
+
if (task) tasks.push(task);
|
|
68
|
+
}
|
|
69
|
+
const movedTaskIds: string[] = [];
|
|
70
|
+
for (const task of tasks) {
|
|
71
|
+
if (task.key === key) continue;
|
|
72
|
+
// A file shared with another task keeps the namespace of the task whose attachment
|
|
73
|
+
// sources its mapping. Mappings with no attachment source follow any attaching task.
|
|
74
|
+
const mappedFiles = await client.query<{ id: string; key: string }>(
|
|
75
|
+
`SELECT DISTINCT m.id, m."key" AS key
|
|
76
|
+
FROM asset_key_mappings m
|
|
77
|
+
JOIN task_attachments a
|
|
78
|
+
ON m.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
|
|
79
|
+
AND m.provider_org_id = COALESCE(a.agent_fs_org_id, '')
|
|
80
|
+
AND m.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
|
|
81
|
+
AND m.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
|
|
82
|
+
WHERE a.task_id = ?
|
|
83
|
+
AND (m.source_entity_type IS NOT 'task-attachment'
|
|
84
|
+
OR m.source_entity_id IN (SELECT id FROM task_attachments WHERE task_id = ?))`,
|
|
85
|
+
[task.id, task.id],
|
|
86
|
+
);
|
|
87
|
+
// The namespace is metadata, and lastUpdatedAt tracks task activity, as in updateTaskTitle.
|
|
88
|
+
await client.run('UPDATE agent_tasks SET "key" = ?, updated_by = ? WHERE id = ?', [
|
|
89
|
+
key,
|
|
90
|
+
changedBy ?? null,
|
|
91
|
+
task.id,
|
|
92
|
+
]);
|
|
93
|
+
for (const mapping of mappedFiles) {
|
|
94
|
+
if (mapping.key === key) continue;
|
|
95
|
+
await client.run(
|
|
96
|
+
'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = ? WHERE id = ?',
|
|
97
|
+
[key, now, changedBy ?? null, mapping.id],
|
|
98
|
+
);
|
|
99
|
+
await insertAssetKeyHistory({
|
|
100
|
+
entityType: "file",
|
|
101
|
+
entityId: mapping.id,
|
|
102
|
+
previousKey: mapping.key,
|
|
103
|
+
newKey: key,
|
|
104
|
+
changedBy,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
await insertAssetKeyHistory({
|
|
108
|
+
entityType: "task",
|
|
109
|
+
entityId: task.id,
|
|
110
|
+
previousKey: task.key,
|
|
111
|
+
newKey: key,
|
|
112
|
+
changedBy,
|
|
113
|
+
});
|
|
114
|
+
movedTaskIds.push(task.id);
|
|
115
|
+
}
|
|
116
|
+
return movedTaskIds;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type SessionKeyMove = { rootId: string; taskIds: string[]; key: string };
|
|
120
|
+
|
|
121
|
+
/** The caller must complete the key audit before this operation. */
|
|
122
|
+
export async function moveSessionChains(input: {
|
|
123
|
+
moves: SessionKeyMove[];
|
|
124
|
+
changedBy?: string;
|
|
125
|
+
}): Promise<string[]> {
|
|
126
|
+
const moved: string[] = [];
|
|
127
|
+
for (const move of input.moves) {
|
|
128
|
+
const taskIds = await getDbClient().transaction(async () =>
|
|
129
|
+
moveTaskChainKeyInTx(move.rootId, move.taskIds, move.key, input.changedBy),
|
|
130
|
+
);
|
|
131
|
+
if (taskIds && taskIds.length > 0) moved.push(move.rootId);
|
|
132
|
+
}
|
|
133
|
+
return moved;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function listPrefixRoots(input: {
|
|
137
|
+
from: string;
|
|
138
|
+
userId?: string | null;
|
|
139
|
+
limit: number;
|
|
140
|
+
offset?: number;
|
|
141
|
+
}): Promise<{ id: string; key: string }[]> {
|
|
142
|
+
const { lo, hi } = keyPrefixRange(input.from);
|
|
143
|
+
return getDbClient().query<{ id: string; key: string }>(
|
|
144
|
+
`SELECT r.id, r."key" FROM agent_tasks r
|
|
145
|
+
WHERE r.parentTaskId IS NULL AND r."key" >= ? AND r."key" < ?
|
|
146
|
+
${input.userId !== undefined ? "AND r.requestedByUserId = ?" : ""}
|
|
147
|
+
ORDER BY r.id LIMIT ? OFFSET ?`,
|
|
148
|
+
[lo, hi, ...(input.userId !== undefined ? [input.userId] : []), input.limit, input.offset ?? 0],
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function listSharedPrefixRoots(input: {
|
|
153
|
+
from: string;
|
|
154
|
+
userId: string | null;
|
|
155
|
+
}): Promise<{ id: string }[]> {
|
|
156
|
+
const { lo, hi } = keyPrefixRange(input.from);
|
|
157
|
+
const visibility = sessionViewerCondition({ userId: input.userId, scope: "shared" });
|
|
158
|
+
return getDbClient().query<{ id: string }>(
|
|
159
|
+
`SELECT r.id FROM agent_tasks r
|
|
160
|
+
WHERE r.parentTaskId IS NULL AND r."key" >= ? AND r."key" < ?
|
|
161
|
+
AND ${visibility.sql} ORDER BY r.id`,
|
|
162
|
+
[lo, hi, ...visibility.params],
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
keyPrefixRange,
|
|
3
|
+
normalizeAssetKey,
|
|
4
|
+
SESSION_LEAF_KINDS,
|
|
5
|
+
sessionGroupKey,
|
|
6
|
+
} from "../../assets/key";
|
|
7
|
+
import { isTerminalTaskStatus, TERMINAL_TASK_STATUSES } from "../../types";
|
|
8
|
+
import { getDbClient, literalPrefixPattern } from "../db";
|
|
9
|
+
import { type SessionViewer, sessionViewerCondition } from "./session-visibility";
|
|
10
|
+
|
|
11
|
+
export type SessionReadState = "unread" | "read" | "later" | "done";
|
|
12
|
+
export interface SessionFilters {
|
|
13
|
+
/** Restrict roots to these task sources. */
|
|
14
|
+
source?: string[];
|
|
15
|
+
/** Match root task text or title with a case-insensitive substring. */
|
|
16
|
+
q?: string;
|
|
17
|
+
/** Match the root requester, excluding NULL requester rows. */
|
|
18
|
+
requestedByUserId?: string;
|
|
19
|
+
/** Literal context prefix for the UI session panel, escaping %, _ and backslash. */
|
|
20
|
+
contextKeyPrefix?: string;
|
|
21
|
+
/** Apply this user's session ownership and share visibility. */
|
|
22
|
+
viewer?: SessionViewer;
|
|
23
|
+
/** Match roots under this literal canonical asset key prefix. */
|
|
24
|
+
keyPrefix?: string;
|
|
25
|
+
/** Match this exact group and its direct resource leaves. */
|
|
26
|
+
groupKey?: string;
|
|
27
|
+
/** Match shared and viewer-personal namespace roots and direct leaves. */
|
|
28
|
+
noGroup?: boolean;
|
|
29
|
+
/** Filter by chain-wide read state or the Inbox section. */
|
|
30
|
+
triage?: "inbox" | "later" | "done" | "unread";
|
|
31
|
+
/** Filter by latest chain status or pending human attention. */
|
|
32
|
+
status?: "running" | "failed" | "needs";
|
|
33
|
+
/** Match a requester anywhere in the chain or an active share recipient. */
|
|
34
|
+
withUserId?: string;
|
|
35
|
+
/** Use this ISO timestamp for read-state comparisons. */
|
|
36
|
+
now?: string;
|
|
37
|
+
/** Exclude ownerless roots for user viewers to match the tree's visibility. */
|
|
38
|
+
tree?: boolean;
|
|
39
|
+
/** Match exactly this root (one session's summary, e.g. its read state). */
|
|
40
|
+
rootId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type SqlFragment = { sql: string; params: (string | number | null)[] };
|
|
44
|
+
|
|
45
|
+
/** Shared read-state rule with bound ISO timestamps for the list and tree. */
|
|
46
|
+
function sessionReadStateSql(now = new Date().toISOString()): SqlFragment {
|
|
47
|
+
return {
|
|
48
|
+
sql: `CASE
|
|
49
|
+
WHEN s.id IS NULL THEN CASE WHEN a.lastActivityAt > ? THEN 'unread' ELSE 'read' END
|
|
50
|
+
WHEN s.status = 'dismissed' THEN 'done'
|
|
51
|
+
WHEN s.status = 'done' THEN CASE WHEN a.lastActivityAt > s.lastUpdatedAt THEN 'unread' ELSE 'done' END
|
|
52
|
+
WHEN s.status = 'snoozed' THEN CASE WHEN s.snoozeUntil > ? THEN 'later' ELSE 'unread' END
|
|
53
|
+
WHEN a.lastActivityAt > COALESCE(s.lastReadAt, s.lastUpdatedAt) THEN 'unread'
|
|
54
|
+
ELSE 'read'
|
|
55
|
+
END`,
|
|
56
|
+
params: [new Date(Date.parse(now) - 7 * 86400000).toISOString(), now],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function groupCondition(group: string): SqlFragment {
|
|
61
|
+
const { lo, hi } = keyPrefixRange(group);
|
|
62
|
+
// The remaining key must be one resource leaf, with no nested path.
|
|
63
|
+
// SQLite substr/length count code points, so count them here too (not UTF-16 units).
|
|
64
|
+
const start = [...lo].length + 1;
|
|
65
|
+
return {
|
|
66
|
+
sql: `(r."key" >= ? AND r."key" < ? AND (r."key" = ? OR (
|
|
67
|
+
instr(substr(r."key", ?, length(r."key") - ?), '/') = 0 AND
|
|
68
|
+
(${SESSION_LEAF_KINDS.map(() => 'substr(r."key", ?) GLOB ?').join(" OR ")})
|
|
69
|
+
)))`,
|
|
70
|
+
params: [
|
|
71
|
+
lo,
|
|
72
|
+
hi,
|
|
73
|
+
lo,
|
|
74
|
+
start,
|
|
75
|
+
start,
|
|
76
|
+
...SESSION_LEAF_KINDS.flatMap((kind) => [start, `${kind}:?*/`]),
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
|
|
82
|
+
const conditions = ["r.parentTaskId IS NULL"];
|
|
83
|
+
const params: SqlFragment["params"] = [];
|
|
84
|
+
const add = (fragment: SqlFragment) => {
|
|
85
|
+
conditions.push(fragment.sql);
|
|
86
|
+
params.push(...fragment.params);
|
|
87
|
+
};
|
|
88
|
+
if (opts.rootId) add({ sql: "r.id = ?", params: [opts.rootId] });
|
|
89
|
+
const sources = opts.source?.filter(Boolean) ?? [];
|
|
90
|
+
if (sources.length)
|
|
91
|
+
add({ sql: `r.source IN (${sources.map(() => "?").join(",")})`, params: sources });
|
|
92
|
+
if (opts.q?.trim()) {
|
|
93
|
+
const like = `%${opts.q.trim().toLowerCase()}%`;
|
|
94
|
+
add({
|
|
95
|
+
sql: "(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)",
|
|
96
|
+
params: [like, like],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (opts.requestedByUserId?.trim())
|
|
100
|
+
add({ sql: "r.requestedByUserId = ?", params: [opts.requestedByUserId.trim()] });
|
|
101
|
+
if (opts.contextKeyPrefix)
|
|
102
|
+
add({
|
|
103
|
+
sql: "r.contextKey LIKE ? ESCAPE '\\'",
|
|
104
|
+
params: [literalPrefixPattern(opts.contextKeyPrefix)],
|
|
105
|
+
});
|
|
106
|
+
if (opts.viewer) {
|
|
107
|
+
add(sessionViewerCondition(opts.viewer));
|
|
108
|
+
if (opts.tree) conditions.push("r.requestedByUserId IS NOT NULL");
|
|
109
|
+
}
|
|
110
|
+
if (opts.keyPrefix) {
|
|
111
|
+
const { lo, hi } = keyPrefixRange(opts.keyPrefix);
|
|
112
|
+
add({ sql: 'r."key" >= ? AND r."key" < ?', params: [lo, hi] });
|
|
113
|
+
}
|
|
114
|
+
if (opts.groupKey) add(groupCondition(normalizeAssetKey(opts.groupKey)));
|
|
115
|
+
if (opts.noGroup) {
|
|
116
|
+
const groups = [groupCondition("shared/")];
|
|
117
|
+
if (opts.viewer?.userId) groups.push(groupCondition(`personal/${opts.viewer.userId}/`));
|
|
118
|
+
add({
|
|
119
|
+
sql: `(${groups.map((g) => g.sql).join(" OR ")})`,
|
|
120
|
+
params: groups.flatMap((g) => g.params),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (opts.triage === "later" || opts.triage === "done") {
|
|
124
|
+
add({
|
|
125
|
+
sql: `r.id IN (SELECT itemId FROM inbox_item_state
|
|
126
|
+
WHERE userId = ? AND itemType = 'to_read' AND status IN (${opts.triage === "later" ? "'snoozed'" : "'done','dismissed'"}))`,
|
|
127
|
+
params: [opts.viewer?.userId ?? null],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { sql: conditions.join(" AND "), params };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function excludesTriaged(opts: SessionFilters): boolean {
|
|
134
|
+
return !!(opts.viewer?.userId && (opts.groupKey || opts.keyPrefix || opts.noGroup));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasSessionChainFilter(opts: SessionFilters): boolean {
|
|
138
|
+
return !!(opts.triage || opts.status || opts.withUserId || excludesTriaged(opts));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const runningSql = `latestStatus NOT IN (${TERMINAL_TASK_STATUSES.map((s) => `'${s}'`).join(",")})`;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A session starts at a parentTaskId IS NULL root. One recursive CTE computes
|
|
145
|
+
* chain-wide latest activity for list ordering. Root filters precede recursion.
|
|
146
|
+
* Read state and chain filters follow aggregation.
|
|
147
|
+
*/
|
|
148
|
+
export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
|
|
149
|
+
const root = sessionRootFilter(opts);
|
|
150
|
+
const read = opts.viewer?.userId ? sessionReadStateSql(opts.now) : { sql: "NULL", params: [] };
|
|
151
|
+
const params = [...root.params];
|
|
152
|
+
if (opts.withUserId) params.push(opts.withUserId);
|
|
153
|
+
params.push(...read.params, opts.viewer?.userId ?? null);
|
|
154
|
+
const filters: string[] = [];
|
|
155
|
+
if (opts.triage === "unread") filters.push("readState = 'unread'");
|
|
156
|
+
else if (opts.triage === "later" || opts.triage === "done") {
|
|
157
|
+
filters.push("readState = ?");
|
|
158
|
+
params.push(opts.triage);
|
|
159
|
+
} else if (opts.triage === "inbox" || excludesTriaged(opts)) {
|
|
160
|
+
filters.push("(readState IS NULL OR readState IN ('read','unread'))");
|
|
161
|
+
}
|
|
162
|
+
if (opts.status === "running") filters.push(runningSql);
|
|
163
|
+
if (opts.status === "failed") filters.push("latestStatus = 'failed'");
|
|
164
|
+
if (opts.status === "needs") filters.push("needsYou = 1");
|
|
165
|
+
if (opts.withUserId) {
|
|
166
|
+
filters.push(`(a.withUser > 0 OR EXISTS (SELECT 1 FROM session_shares sh
|
|
167
|
+
WHERE sh.rootTaskId = a.root_id AND sh.revokedAt IS NULL AND sh.userId = ?))`);
|
|
168
|
+
params.push(opts.withUserId);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
// SQLite takes the bare status from the row with MAX(lastUpdatedAt).
|
|
172
|
+
// Keep this as the only MIN/MAX aggregate so status follows activity.
|
|
173
|
+
sql: `WITH RECURSIVE chain(root_id, id, lastUpdatedAt, status, requestedByUserId) AS (
|
|
174
|
+
SELECT r.id, r.id, r.lastUpdatedAt, r.status, r.requestedByUserId
|
|
175
|
+
FROM agent_tasks r WHERE ${root.sql}
|
|
176
|
+
UNION ALL
|
|
177
|
+
SELECT c.root_id, t.id, t.lastUpdatedAt, t.status, t.requestedByUserId
|
|
178
|
+
FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
|
|
179
|
+
), agg AS (
|
|
180
|
+
SELECT root_id, COUNT(*) AS chainCount,
|
|
181
|
+
status AS latestStatus, MAX(lastUpdatedAt) AS lastActivityAt,
|
|
182
|
+
${opts.withUserId ? "SUM(requestedByUserId = ?) AS withUser," : ""}
|
|
183
|
+
SUM(EXISTS (SELECT 1 FROM approval_requests ar
|
|
184
|
+
WHERE ar.sourceTaskId = chain.id AND ar.status = 'pending')) AS pendingApproval
|
|
185
|
+
FROM chain GROUP BY root_id
|
|
186
|
+
), state AS (
|
|
187
|
+
SELECT a.*, ${read.sql} AS readState, s.snoozeUntil
|
|
188
|
+
FROM agg a LEFT JOIN inbox_item_state s
|
|
189
|
+
ON s.userId = ? AND s.itemType = 'to_read' AND s.itemId = a.root_id
|
|
190
|
+
), summary AS (
|
|
191
|
+
SELECT *, (COALESCE(readState = 'unread' AND latestStatus = 'failed', 0) OR pendingApproval) AS needsYou
|
|
192
|
+
FROM state
|
|
193
|
+
)
|
|
194
|
+
SELECT a.* FROM summary a ${filters.length ? `WHERE ${filters.join(" AND ")}` : ""}`,
|
|
195
|
+
params,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One session's read state for a user, with the same rule and chain activity
|
|
201
|
+
* as the list rows. Null when the user cannot see the root.
|
|
202
|
+
*/
|
|
203
|
+
export async function getSessionReadState(
|
|
204
|
+
rootId: string,
|
|
205
|
+
userId: string,
|
|
206
|
+
now?: string,
|
|
207
|
+
): Promise<{ readState: SessionReadState; lastActivityAt: string } | null> {
|
|
208
|
+
const query = sessionSummaryQuery({ rootId, viewer: { userId, scope: "all" }, now });
|
|
209
|
+
const row = await getDbClient().get<{ readState: SessionReadState; lastActivityAt: string }>(
|
|
210
|
+
`SELECT readState, lastActivityAt FROM (${query.sql})`,
|
|
211
|
+
query.params,
|
|
212
|
+
);
|
|
213
|
+
return row?.readState ? { readState: row.readState, lastActivityAt: row.lastActivityAt } : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Count roots directly unless a filter needs chain activity or participants. */
|
|
217
|
+
export function sessionCountQuery(opts: SessionFilters = {}): SqlFragment {
|
|
218
|
+
const chain = hasSessionChainFilter(opts);
|
|
219
|
+
const query = chain ? sessionSummaryQuery(opts) : sessionRootFilter(opts);
|
|
220
|
+
return {
|
|
221
|
+
sql: chain
|
|
222
|
+
? `SELECT COUNT(*) AS count FROM (${query.sql})`
|
|
223
|
+
: `SELECT COUNT(*) AS count FROM agent_tasks r WHERE ${query.sql}`,
|
|
224
|
+
params: query.params,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface SessionRowFields {
|
|
229
|
+
readState?: SessionReadState;
|
|
230
|
+
snoozeUntil?: string | null;
|
|
231
|
+
needsYou?: boolean;
|
|
232
|
+
groupKey?: string;
|
|
233
|
+
shared?: boolean;
|
|
234
|
+
people?: { id: string; name: string }[];
|
|
235
|
+
peopleCount?: number;
|
|
236
|
+
preview?: string | null;
|
|
237
|
+
/** Active shares with their access. `userId` null means everyone on the swarm. */
|
|
238
|
+
shares?: { userId: string | null; name: string | null; access: "read" | "write" }[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Two page-wide queries load people, shares and bounded output previews. */
|
|
242
|
+
export async function sessionPageFields(
|
|
243
|
+
roots: { id: string; requestedByUserId?: string }[],
|
|
244
|
+
): Promise<Map<string, SessionRowFields>> {
|
|
245
|
+
const result = new Map<string, SessionRowFields>();
|
|
246
|
+
if (!roots.length) return result;
|
|
247
|
+
const ids = roots.map((r) => r.id);
|
|
248
|
+
const [tasks, shares] = await Promise.all([
|
|
249
|
+
getDbClient().query<{
|
|
250
|
+
root_id: string;
|
|
251
|
+
id: string | null;
|
|
252
|
+
name: string | null;
|
|
253
|
+
preview: string | null;
|
|
254
|
+
}>(
|
|
255
|
+
`WITH RECURSIVE chain(root_id, id) AS (
|
|
256
|
+
SELECT id, id FROM agent_tasks WHERE id IN (${ids.map(() => "?").join(",")})
|
|
257
|
+
UNION ALL SELECT c.root_id, t.id FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
|
|
258
|
+
) SELECT c.root_id, t.requestedByUserId AS id, u.name,
|
|
259
|
+
CASE WHEN t.source = 'system' AND t.taskType = 'follow-up' THEN NULL ELSE NULLIF(trim(substr(t.output, 1, 200)), '') END AS preview
|
|
260
|
+
FROM chain c CROSS JOIN agent_tasks t ON t.id = c.id
|
|
261
|
+
LEFT JOIN users u ON u.id = t.requestedByUserId
|
|
262
|
+
ORDER BY t.lastUpdatedAt DESC, t.createdAt DESC, t.id DESC`,
|
|
263
|
+
ids,
|
|
264
|
+
),
|
|
265
|
+
getDbClient().query<{
|
|
266
|
+
root_id: string;
|
|
267
|
+
id: string | null;
|
|
268
|
+
name: string | null;
|
|
269
|
+
access: "read" | "write";
|
|
270
|
+
}>(
|
|
271
|
+
`SELECT sh.rootTaskId AS root_id, sh.userId AS id, u.name, sh.access FROM session_shares sh
|
|
272
|
+
LEFT JOIN users u ON u.id = sh.userId
|
|
273
|
+
WHERE sh.revokedAt IS NULL AND sh.rootTaskId IN (${ids.map(() => "?").join(",")})
|
|
274
|
+
ORDER BY sh.createdAt, sh.id`,
|
|
275
|
+
ids,
|
|
276
|
+
),
|
|
277
|
+
]);
|
|
278
|
+
const people = new Map<string, Map<string, string>>();
|
|
279
|
+
for (const root of roots) {
|
|
280
|
+
result.set(root.id, { shared: false, preview: null, shares: [] });
|
|
281
|
+
people.set(root.id, new Map());
|
|
282
|
+
}
|
|
283
|
+
// Chain rows are newest first. Reserve the requester slot before adding other people.
|
|
284
|
+
for (const root of roots) {
|
|
285
|
+
if (!root.requestedByUserId) continue;
|
|
286
|
+
people.get(root.id)!.set(root.requestedByUserId, root.requestedByUserId);
|
|
287
|
+
}
|
|
288
|
+
for (const task of tasks) {
|
|
289
|
+
if (task.id) people.get(task.root_id)!.set(task.id, task.name ?? task.id);
|
|
290
|
+
const fields = result.get(task.root_id)!;
|
|
291
|
+
if (fields.preview === null && task.preview !== null) fields.preview = task.preview;
|
|
292
|
+
}
|
|
293
|
+
for (const share of shares) {
|
|
294
|
+
const fields = result.get(share.root_id)!;
|
|
295
|
+
fields.shared = true;
|
|
296
|
+
fields.shares!.push({ userId: share.id, name: share.name, access: share.access });
|
|
297
|
+
if (share.id) people.get(share.root_id)!.set(share.id, share.name ?? share.id);
|
|
298
|
+
}
|
|
299
|
+
for (const [id, members] of people) {
|
|
300
|
+
Object.assign(result.get(id)!, {
|
|
301
|
+
people: [...members].slice(0, 8).map(([id, name]) => ({ id, name })),
|
|
302
|
+
peopleCount: members.size,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface SessionSectionCounts {
|
|
309
|
+
total: number;
|
|
310
|
+
unread: number;
|
|
311
|
+
running: number;
|
|
312
|
+
needsYou: number;
|
|
313
|
+
}
|
|
314
|
+
export interface SessionTreeGroup extends SessionSectionCounts {
|
|
315
|
+
key: string;
|
|
316
|
+
lastActivityAt: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface SessionTree {
|
|
320
|
+
groups: SessionTreeGroup[];
|
|
321
|
+
inbox: SessionSectionCounts;
|
|
322
|
+
later: SessionSectionCounts;
|
|
323
|
+
done: SessionSectionCounts;
|
|
324
|
+
noGroup: SessionSectionCounts;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function getSessionTree(
|
|
328
|
+
actor: { kind: "bypass" } | { kind: "user"; userId: string | null },
|
|
329
|
+
now?: string,
|
|
330
|
+
): Promise<SessionTree> {
|
|
331
|
+
const query = sessionSummaryQuery({
|
|
332
|
+
...(actor.kind === "user" ? { viewer: { userId: actor.userId, scope: "all" } as const } : {}),
|
|
333
|
+
now,
|
|
334
|
+
tree: true,
|
|
335
|
+
});
|
|
336
|
+
const rows = await getDbClient().query<{
|
|
337
|
+
key: string;
|
|
338
|
+
lastActivityAt: string;
|
|
339
|
+
latestStatus: string;
|
|
340
|
+
readState: SessionReadState | null;
|
|
341
|
+
needsYou: number;
|
|
342
|
+
}>(
|
|
343
|
+
`SELECT r."key", a.lastActivityAt, a.latestStatus, a.readState, a.needsYou
|
|
344
|
+
FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id`,
|
|
345
|
+
query.params,
|
|
346
|
+
);
|
|
347
|
+
const counts = (): SessionSectionCounts => ({ total: 0, unread: 0, running: 0, needsYou: 0 });
|
|
348
|
+
const result: SessionTree = {
|
|
349
|
+
groups: [],
|
|
350
|
+
inbox: counts(),
|
|
351
|
+
later: counts(),
|
|
352
|
+
done: counts(),
|
|
353
|
+
noGroup: counts(),
|
|
354
|
+
};
|
|
355
|
+
const groups = new Map<string, SessionTreeGroup>();
|
|
356
|
+
const add = (section: SessionSectionCounts, row: (typeof rows)[number]) => {
|
|
357
|
+
section.total++;
|
|
358
|
+
if (row.readState === "unread") section.unread++;
|
|
359
|
+
if (!isTerminalTaskStatus(row.latestStatus)) section.running++;
|
|
360
|
+
if (row.needsYou) section.needsYou++;
|
|
361
|
+
};
|
|
362
|
+
for (const row of rows) {
|
|
363
|
+
const key = sessionGroupKey(row.key);
|
|
364
|
+
const noGroup =
|
|
365
|
+
key === "shared/" || (actor.kind === "user" && key === `personal/${actor.userId}/`);
|
|
366
|
+
let group: SessionTreeGroup | undefined;
|
|
367
|
+
if (!noGroup) {
|
|
368
|
+
group = groups.get(key);
|
|
369
|
+
if (!group) {
|
|
370
|
+
group = { key, ...counts(), lastActivityAt: row.lastActivityAt };
|
|
371
|
+
groups.set(key, group);
|
|
372
|
+
}
|
|
373
|
+
if (row.lastActivityAt > group.lastActivityAt) group.lastActivityAt = row.lastActivityAt;
|
|
374
|
+
}
|
|
375
|
+
if (row.readState === "later" || row.readState === "done") add(result[row.readState], row);
|
|
376
|
+
else {
|
|
377
|
+
add(result.inbox, row);
|
|
378
|
+
if (noGroup) add(result.noGroup, row);
|
|
379
|
+
else if (group) add(group, row);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
result.groups = [...groups.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
383
|
+
return result;
|
|
384
|
+
}
|