@desplega.ai/agent-swarm 2.7.4 → 2.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/acp-adapter-g3ads55s.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
- package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.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-0wyynpn1.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
- package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
- package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
- package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
- package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
- package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
- 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-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
- package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
- package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
- package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
- package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
- package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
- package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
- package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
- 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-mvckssyq.js → cli-g61qxmge.js} +5 -5
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
- package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
- package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
- package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
- package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
- package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
- package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
- package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
- package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
- package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
- package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
- package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
- package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
- package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
- package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
- package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
- package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
- package/dist/cli-zh3fp8v2.js +151 -0
- package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.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-3mwmk3d9.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
- package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
- package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
- package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
- package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
- package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
- package/dist/fx-adapter-gt6pncj2.js +18 -0
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
- package/dist/handlers-vwmba1m2.js +31 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
- package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
- package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
- package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
- package/dist/index-5r71rf98.js +32 -0
- package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
- package/dist/index-anmwxs87.js +29 -0
- package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
- package/dist/index-spp0ra5c.js +36 -0
- package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
- package/dist/lead-fjy418cw.js +69 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
- package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
- package/dist/worker-0c5tyzqe.js +69 -0
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +97 -32
- package/package.json +1 -1
- 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.ts +8 -4
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/scripts/typecheck.ts +1 -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/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -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/mcp-bridge.ts +4 -0
- 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/scheduler/scheduler.ts +36 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +5 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/handlers.ts +18 -5
- package/src/slack/left-threads.ts +65 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- 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/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/explicit-model-validation.test.ts +99 -0
- 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/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/mcp-loopback-proxy.test.ts +193 -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 +3 -2
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -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 +431 -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/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +19 -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/swarm-docs/content.md +1 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TERMINAL_TASK_STATUSES } from "../../types";
|
|
1
2
|
import { getDbClient } from "./runtime";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -37,18 +38,20 @@ export interface SessionSummaryDrift {
|
|
|
37
38
|
| "lastActivityAt"
|
|
38
39
|
| "latestStatus"
|
|
39
40
|
| "pendingApproval"
|
|
41
|
+
| "openTasks"
|
|
40
42
|
| "lastHumanAt"
|
|
41
43
|
| "member";
|
|
42
44
|
expected: string | number | null;
|
|
43
45
|
stored: string | number | null;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
const OPEN_TASK_SQL = `t.status NOT IN (${TERMINAL_TASK_STATUSES.map((s) => `'${s}'`).join(", ")})`;
|
|
46
49
|
const HUMAN_TASK_SQL = `t.requestedByUserId IS NOT NULL AND t.requestedByUserIdInherited = 0
|
|
47
50
|
AND COALESCE(t.source, '') NOT IN ('mcp', 'system', 'schedule', 'workflow')`;
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* Read-only drift check: compares the stored summaries and members with a
|
|
51
|
-
* fresh chain walk (the rule `
|
|
54
|
+
* fresh chain walk (the rule `legacySessionSummaryQuery` applies). Returns one
|
|
52
55
|
* row per mismatch, so an empty result means the tables are exact. With
|
|
53
56
|
* `since`, only roots whose chain activity is at or after it are compared.
|
|
54
57
|
* `latestStatus` is skipped when two chain tasks tie on the newest
|
|
@@ -64,7 +67,7 @@ export function sessionSummaryDriftQuery(since: string | null = null): {
|
|
|
64
67
|
UNION ALL SELECT c.root_id, t.id FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
|
|
65
68
|
), walked AS (
|
|
66
69
|
SELECT c.root_id, COUNT(*) AS chainCount, t.status AS latestStatus,
|
|
67
|
-
MAX(t.lastUpdatedAt) AS lastActivityAt
|
|
70
|
+
MAX(t.lastUpdatedAt) AS lastActivityAt, SUM(${OPEN_TASK_SQL}) AS openTasks
|
|
68
71
|
FROM chain c JOIN agent_tasks t ON t.id = c.id GROUP BY c.root_id
|
|
69
72
|
), expected AS (
|
|
70
73
|
SELECT w.*,
|
|
@@ -98,6 +101,9 @@ export function sessionSummaryDriftQuery(since: string | null = null): {
|
|
|
98
101
|
UNION ALL SELECT e.root_id, 'pendingApproval', e.pendingApproval, s.pendingApprovalCount > 0
|
|
99
102
|
FROM expected e JOIN session_summaries s ON s.rootId = e.root_id
|
|
100
103
|
WHERE (s.pendingApprovalCount > 0) IS NOT e.pendingApproval
|
|
104
|
+
UNION ALL SELECT e.root_id, 'openTasks', e.openTasks, s.openTaskCount
|
|
105
|
+
FROM expected e JOIN session_summaries s ON s.rootId = e.root_id
|
|
106
|
+
WHERE s.openTaskCount IS NOT e.openTasks
|
|
101
107
|
UNION ALL SELECT e.root_id, 'lastHumanAt', e.lastHumanAt, s.lastHumanAt
|
|
102
108
|
FROM expected e JOIN session_summaries s ON s.rootId = e.root_id
|
|
103
109
|
WHERE s.lastHumanAt IS NOT e.lastHumanAt
|
|
@@ -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.ts
CHANGED
|
@@ -6283,7 +6283,7 @@ export interface UpdateScheduledTaskData {
|
|
|
6283
6283
|
description?: string;
|
|
6284
6284
|
cronExpression?: string | null;
|
|
6285
6285
|
intervalMs?: number | null;
|
|
6286
|
-
taskTemplate?: string;
|
|
6286
|
+
taskTemplate?: string | null;
|
|
6287
6287
|
taskType?: string;
|
|
6288
6288
|
tags?: string[];
|
|
6289
6289
|
priority?: number;
|
|
@@ -13073,6 +13073,9 @@ export async function listRecentSessions(
|
|
|
13073
13073
|
): Promise<SessionListItem[] | SessionListItemSummary[]> {
|
|
13074
13074
|
const query = sessionSummaryQuery(opts);
|
|
13075
13075
|
const threadRoots = threadRootsSql();
|
|
13076
|
+
const order = (a: string) =>
|
|
13077
|
+
`${opts.triage === "later" ? `${a}.snoozeUntil ASC,` : ""}
|
|
13078
|
+
${a}.lastActivityAt DESC, ${a}.rootCreatedAt DESC, ${a}.root_id DESC`;
|
|
13076
13079
|
const rootRows = await getDbClient().query<
|
|
13077
13080
|
AgentTaskRow & {
|
|
13078
13081
|
__chainCount: number;
|
|
@@ -13084,13 +13087,14 @@ export async function listRecentSessions(
|
|
|
13084
13087
|
__threadRoots: number;
|
|
13085
13088
|
}
|
|
13086
13089
|
>(
|
|
13090
|
+
// Page first on the summaries, then read agent_tasks for the page roots only.
|
|
13087
13091
|
`SELECT r.*, a.chainCount AS __chainCount, a.lastActivityAt AS __lastActivityAt,
|
|
13088
13092
|
a.latestStatus AS __latestStatus, a.readState AS __readState,
|
|
13089
13093
|
a.snoozeUntil AS __snoozeUntil, a.needsYou AS __needsYou,
|
|
13090
13094
|
${threadRoots.sql} AS __threadRoots
|
|
13091
|
-
FROM (${query.sql})
|
|
13092
|
-
|
|
13093
|
-
|
|
13095
|
+
FROM (SELECT * FROM (${query.sql}) page ORDER BY ${order("page")} LIMIT ? OFFSET ?) a
|
|
13096
|
+
CROSS JOIN agent_tasks r ON r.id = a.root_id
|
|
13097
|
+
ORDER BY ${order("a")}`,
|
|
13094
13098
|
[...threadRoots.params, ...query.params, opts.limit ?? 25, opts.offset ?? 0],
|
|
13095
13099
|
);
|
|
13096
13100
|
const fields = await sessionPageFields(
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
-- What the session read path needs from session_summaries (migration 253):
|
|
2
|
+
-- the key and source indexes its filters use, and openTaskCount, the number
|
|
3
|
+
-- of chain tasks that are not finished (status outside completed, failed,
|
|
4
|
+
-- cancelled, superseded).
|
|
5
|
+
--
|
|
6
|
+
-- Auto-Done (src/be/db/session-tree.ts) moves a quiet Inbox session to Done
|
|
7
|
+
-- only when every chain task is finished. The session read path now reads
|
|
8
|
+
-- session_summaries instead of walking the chain, so the summary carries the
|
|
9
|
+
-- count. Same consistency rules as migration 253:
|
|
10
|
+
-- - Inserts add the new task's count incrementally.
|
|
11
|
+
-- - A status change applies its +1 or -1 in a BEFORE trigger. An AFTER
|
|
12
|
+
-- trigger of the same statement that recomputes the chain (a rewound
|
|
13
|
+
-- lastUpdatedAt, a requester or source edit) then overwrites it with the
|
|
14
|
+
-- exact count, so the delta is never applied on top of a recompute.
|
|
15
|
+
-- - session_summary_recompute_apply recomputes it with the other aggregates.
|
|
16
|
+
|
|
17
|
+
ALTER TABLE session_summaries ADD COLUMN openTaskCount INTEGER NOT NULL DEFAULT 0;
|
|
18
|
+
|
|
19
|
+
-- The group (key range) and source filters of the list and count. Activity
|
|
20
|
+
-- writes do not touch either column, so neither index costs the hot path.
|
|
21
|
+
CREATE INDEX idx_session_summaries_key ON session_summaries("key");
|
|
22
|
+
CREATE INDEX idx_session_summaries_source ON session_summaries(source);
|
|
23
|
+
|
|
24
|
+
DROP TRIGGER session_summary_recompute_apply;
|
|
25
|
+
CREATE TRIGGER session_summary_recompute_apply
|
|
26
|
+
INSTEAD OF INSERT ON session_summary_recompute
|
|
27
|
+
BEGIN
|
|
28
|
+
UPDATE session_summaries SET
|
|
29
|
+
chainCount = (SELECT COUNT(*) FROM session_members m WHERE m.rootId = NEW.rootId),
|
|
30
|
+
lastActivityAt = (SELECT MAX(t.lastUpdatedAt) FROM session_members m
|
|
31
|
+
JOIN agent_tasks t ON t.id = m.taskId WHERE m.rootId = NEW.rootId),
|
|
32
|
+
latestStatus = (SELECT t.status FROM session_members m
|
|
33
|
+
JOIN agent_tasks t ON t.id = m.taskId WHERE m.rootId = NEW.rootId
|
|
34
|
+
ORDER BY t.lastUpdatedAt DESC LIMIT 1),
|
|
35
|
+
pendingApprovalCount = (SELECT COUNT(*) FROM session_members m
|
|
36
|
+
JOIN approval_requests ar ON ar.sourceTaskId = m.taskId
|
|
37
|
+
WHERE m.rootId = NEW.rootId AND ar.status = 'pending'),
|
|
38
|
+
openTaskCount = (SELECT COUNT(*) FROM session_members m
|
|
39
|
+
JOIN agent_tasks t ON t.id = m.taskId WHERE m.rootId = NEW.rootId
|
|
40
|
+
AND t.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')),
|
|
41
|
+
lastHumanAt = (SELECT MAX(at) FROM (
|
|
42
|
+
SELECT t.createdAt AS at FROM session_members m JOIN agent_tasks t ON t.id = m.taskId
|
|
43
|
+
WHERE m.rootId = NEW.rootId AND t.requestedByUserId IS NOT NULL
|
|
44
|
+
AND t.requestedByUserIdInherited = 0
|
|
45
|
+
AND COALESCE(t.source, '') NOT IN ('mcp', 'system', 'schedule', 'workflow')
|
|
46
|
+
UNION ALL
|
|
47
|
+
SELECT sm.created_at FROM session_members m
|
|
48
|
+
JOIN task_steering_messages sm ON sm.task_id = m.taskId
|
|
49
|
+
WHERE m.rootId = NEW.rootId AND sm.created_by_kind = 'user' AND sm.status != 'cancelled'
|
|
50
|
+
UNION ALL
|
|
51
|
+
SELECT ar.createdAt FROM session_members m
|
|
52
|
+
JOIN approval_requests ar ON ar.sourceTaskId = m.taskId
|
|
53
|
+
WHERE m.rootId = NEW.rootId AND ar.status = 'pending'))
|
|
54
|
+
WHERE rootId = NEW.rootId
|
|
55
|
+
AND EXISTS (SELECT 1 FROM agent_tasks WHERE id = NEW.rootId);
|
|
56
|
+
END;
|
|
57
|
+
|
|
58
|
+
DROP TRIGGER session_task_insert;
|
|
59
|
+
CREATE TRIGGER session_task_insert
|
|
60
|
+
AFTER INSERT ON agent_tasks
|
|
61
|
+
BEGIN
|
|
62
|
+
INSERT OR REPLACE INTO session_members (taskId, rootId)
|
|
63
|
+
SELECT NEW.id, NEW.id WHERE NEW.parentTaskId IS NULL
|
|
64
|
+
UNION ALL
|
|
65
|
+
SELECT NEW.id, m.rootId FROM session_members m WHERE m.taskId = NEW.parentTaskId;
|
|
66
|
+
|
|
67
|
+
INSERT OR REPLACE INTO session_summaries (rootId, createdAt, source, "key", requestedByUserId,
|
|
68
|
+
scheduleId, taskType, slackChannelId, contextKey, chainCount, lastActivityAt, latestStatus,
|
|
69
|
+
pendingApprovalCount, openTaskCount, lastHumanAt)
|
|
70
|
+
SELECT NEW.id, NEW.createdAt, NEW.source, NEW."key", NEW.requestedByUserId, NEW.scheduleId,
|
|
71
|
+
NEW.taskType, NEW.slackChannelId, NEW.contextKey, 1, NEW.lastUpdatedAt, NEW.status, 0,
|
|
72
|
+
NEW.status NOT IN ('completed', 'failed', 'cancelled', 'superseded'),
|
|
73
|
+
CASE WHEN NEW.requestedByUserId IS NOT NULL AND NEW.requestedByUserIdInherited = 0
|
|
74
|
+
AND COALESCE(NEW.source, '') NOT IN ('mcp', 'system', 'schedule', 'workflow')
|
|
75
|
+
THEN NEW.createdAt END
|
|
76
|
+
WHERE NEW.parentTaskId IS NULL;
|
|
77
|
+
|
|
78
|
+
UPDATE session_summaries SET
|
|
79
|
+
chainCount = chainCount + 1,
|
|
80
|
+
openTaskCount = openTaskCount
|
|
81
|
+
+ (NEW.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')),
|
|
82
|
+
latestStatus = CASE WHEN NEW.lastUpdatedAt >= lastActivityAt THEN NEW.status ELSE latestStatus END,
|
|
83
|
+
lastActivityAt = MAX(lastActivityAt, NEW.lastUpdatedAt),
|
|
84
|
+
lastHumanAt = CASE WHEN NEW.requestedByUserId IS NOT NULL AND NEW.requestedByUserIdInherited = 0
|
|
85
|
+
AND COALESCE(NEW.source, '') NOT IN ('mcp', 'system', 'schedule', 'workflow')
|
|
86
|
+
AND (lastHumanAt IS NULL OR NEW.createdAt > lastHumanAt)
|
|
87
|
+
THEN NEW.createdAt ELSE lastHumanAt END
|
|
88
|
+
WHERE NEW.parentTaskId IS NOT NULL
|
|
89
|
+
AND rootId = (SELECT rootId FROM session_members WHERE taskId = NEW.id);
|
|
90
|
+
END;
|
|
91
|
+
|
|
92
|
+
-- BEFORE, so an AFTER recompute in the same statement has the last word.
|
|
93
|
+
CREATE TRIGGER session_task_open
|
|
94
|
+
BEFORE UPDATE OF status ON agent_tasks
|
|
95
|
+
WHEN (OLD.status NOT IN ('completed', 'failed', 'cancelled', 'superseded'))
|
|
96
|
+
IS NOT (NEW.status NOT IN ('completed', 'failed', 'cancelled', 'superseded'))
|
|
97
|
+
BEGIN
|
|
98
|
+
UPDATE session_summaries SET
|
|
99
|
+
openTaskCount = openTaskCount
|
|
100
|
+
+ CASE WHEN NEW.status NOT IN ('completed', 'failed', 'cancelled', 'superseded') THEN 1 ELSE -1 END
|
|
101
|
+
WHERE rootId = (SELECT rootId FROM session_members WHERE taskId = NEW.id);
|
|
102
|
+
END;
|
|
103
|
+
|
|
104
|
+
-- Backfill: one pass over the unfinished tasks.
|
|
105
|
+
UPDATE session_summaries SET openTaskCount = unfinished.n
|
|
106
|
+
FROM (
|
|
107
|
+
SELECT m.rootId, COUNT(*) AS n FROM agent_tasks t JOIN session_members m ON m.taskId = t.id
|
|
108
|
+
WHERE t.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
109
|
+
GROUP BY m.rootId
|
|
110
|
+
) AS unfinished
|
|
111
|
+
WHERE unfinished.rootId = session_summaries.rootId;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - a `latest:<provider>/<family>` alias that is valid grammar and resolves to a catalog model;
|
|
8
8
|
* - a catalog id, bare (`claude-opus-5-5`, `gpt-5.6-sol`) or provider-qualified
|
|
9
9
|
* (`openrouter/deepseek/deepseek-v4.1-flash`, `amazon-bedrock/...`);
|
|
10
|
-
* - a Claude CLI shortname (`opus`, `sonnet`, ...), which the CLI resolves itself
|
|
10
|
+
* - a Claude CLI shortname (`opus`, `sonnet`, ...), which the CLI resolves itself;
|
|
11
|
+
* - a ChatGPT subscription id `openai-codex/<id>` whose `<id>` is in the `openai` section.
|
|
11
12
|
*
|
|
12
13
|
* Escape hatch: `allowCustomModel: true` on the call (tools, REST bodies, workflow node
|
|
13
14
|
* config, the agent runtime PATCH). Custom ids skip the check and are stored as given, for
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
*/
|
|
24
25
|
import {
|
|
25
26
|
buildClaudeShortnameMap,
|
|
27
|
+
chatgptSubscriptionModelId,
|
|
26
28
|
type HarnessCatalogSections,
|
|
27
29
|
harnessModelMismatch,
|
|
28
30
|
isAlias,
|
|
@@ -55,6 +57,8 @@ function hasModel(catalog: CatalogSections, provider: string, modelId: string):
|
|
|
55
57
|
export function isKnownCatalogModel(model: string, catalog: CatalogSections): boolean {
|
|
56
58
|
const id = model.trim().replace(CONTEXT_SUFFIX_RE, "");
|
|
57
59
|
if (!id) return false;
|
|
60
|
+
const subscriptionId = chatgptSubscriptionModelId(id);
|
|
61
|
+
if (subscriptionId) return hasModel(catalog, "openai", subscriptionId);
|
|
58
62
|
const slash = id.indexOf("/");
|
|
59
63
|
if (slash > 0) {
|
|
60
64
|
const provider = id.slice(0, slash);
|