@desplega.ai/agent-swarm 2.5.1 → 2.5.3
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/dist/{acp-adapter-7g6bpwbk.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-w8xd014k.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-nndcvftq.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-fkbyk66x.js} +6905 -6855
- package/dist/{cli-y3zyaee5.js → cli-1fc85mvt.js} +5 -5
- package/dist/{cli-v2g93k4v.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-sn1vbtg2.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-qfm6my86.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-xd4eqtr2.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-z2dv25mf.js → cli-99xgw26r.js} +116 -5
- package/dist/{cli-jpxsrxcb.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-aecdcr77.js} +343 -325
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-e9btqw4y.js → cli-ce0ak9ms.js} +18 -13
- package/dist/{cli-nj12tn2c.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-gwcf52gz.js → cli-e8jyq7d8.js} +11 -7
- package/dist/{cli-rngde2jv.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-9mm5ade4.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-3ee5z0q0.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-eym3jg7w.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-cwyafhw1.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-j1eghqfm.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-pyy4c9ch.js → cli-jjznk3f7.js} +881 -232
- package/dist/{cli-q9pzwxvk.js → cli-kxawjrxg.js} +222 -37
- package/dist/{cli-rh5smt8t.js → cli-mz9afxxf.js} +14 -3
- package/dist/{cli-mz2ygeyq.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-fes51ya2.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-mq7kg1b1.js → cli-p9rafdrb.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-2wba1ac9.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-znvq0k3t.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-xc65x52q.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-0jy85zz4.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-1dp1zj04.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-ajqhxtnz.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-y561h5a9.js} +7 -6
- package/dist/{cli-2jg61wab.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +19 -18
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-88wcymxt.js → db-24ck09xy.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-606ydx5v.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-y8q5z8z2.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-rk9dqech.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-b1mez1m8.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ffevzed1.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-mxcqhhcs.js} +3020 -2463
- package/dist/{index-5en23s5z.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-mj46rk9k.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-nq8hhv28.js → index-n1vbvta9.js} +19 -10
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-d2qeem97.js → index-spwxjgay.js} +17 -17
- package/dist/{index-4q3vj1yr.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-wsby1btq.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-vnshcyv9.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-682k2mt5.js} +12 -2
- package/dist/rbac-roles-gfn30fb8.js +26 -0
- package/dist/{render-v2-pyfsg9zk.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-zsfqzxhm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3163 -2114
- package/package.json +7 -6
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +158 -13
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +47 -7
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +27 -5
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/be/users.ts +28 -4
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +254 -43
- package/src/http/config.ts +22 -37
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +118 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +42 -6
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +183 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +10 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +410 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +23 -0
- package/src/workflows/approval-notifications.ts +221 -0
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +36 -173
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -6,12 +6,23 @@ import {
|
|
|
6
6
|
} from "../../assets/key";
|
|
7
7
|
import { isTerminalTaskStatus, TERMINAL_TASK_STATUSES } from "../../types";
|
|
8
8
|
import { getDbClient, literalPrefixPattern } from "../db";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type SessionViewer,
|
|
11
|
+
sessionMineCondition,
|
|
12
|
+
sessionViewerCondition,
|
|
13
|
+
} from "./session-visibility";
|
|
10
14
|
|
|
11
15
|
export type SessionReadState = "unread" | "read" | "later" | "done";
|
|
12
16
|
export interface SessionFilters {
|
|
13
17
|
/** Restrict roots to these task sources. */
|
|
14
18
|
source?: string[];
|
|
19
|
+
/** Leave out roots of these task sources (the older sessions v2 source filter). */
|
|
20
|
+
hideSource?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Keep only roots of these task sources (the sessions v2 source filter).
|
|
23
|
+
* Unlike `source`, an empty list matches no root.
|
|
24
|
+
*/
|
|
25
|
+
showSource?: string[];
|
|
15
26
|
/** Match root task text or title with a case-insensitive substring. */
|
|
16
27
|
q?: string;
|
|
17
28
|
/** Match the root requester, excluding NULL requester rows. */
|
|
@@ -36,19 +47,25 @@ export interface SessionFilters {
|
|
|
36
47
|
now?: string;
|
|
37
48
|
/** Exclude ownerless roots for user viewers to match the tree's visibility. */
|
|
38
49
|
tree?: boolean;
|
|
50
|
+
/** For user viewers: only human-started roots of the viewer and shares (the "Mine" view). */
|
|
51
|
+
mine?: boolean;
|
|
39
52
|
/** Match exactly this root (one session's summary, e.g. its read state). */
|
|
40
53
|
rootId?: string;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
type SqlFragment = { sql: string; params: (string | number | null)[] };
|
|
44
57
|
|
|
45
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Shared read-state rule with bound ISO timestamps for the list and tree.
|
|
60
|
+
* Done holds through agent activity (progress, status changes, child tasks):
|
|
61
|
+
* only `a.reopensDone`, counted in `sessionSummaryQuery`, brings it back.
|
|
62
|
+
*/
|
|
46
63
|
function sessionReadStateSql(now = new Date().toISOString()): SqlFragment {
|
|
47
64
|
return {
|
|
48
65
|
sql: `CASE
|
|
49
66
|
WHEN s.id IS NULL THEN CASE WHEN a.lastActivityAt > ? THEN 'unread' ELSE 'read' END
|
|
50
67
|
WHEN s.status = 'dismissed' THEN 'done'
|
|
51
|
-
WHEN s.status = 'done' THEN CASE WHEN a.
|
|
68
|
+
WHEN s.status = 'done' THEN CASE WHEN a.reopensDone > 0 THEN 'unread' ELSE 'done' END
|
|
52
69
|
WHEN s.status = 'snoozed' THEN CASE WHEN s.snoozeUntil > ? THEN 'later' ELSE 'unread' END
|
|
53
70
|
WHEN a.lastActivityAt > COALESCE(s.lastReadAt, s.lastUpdatedAt) THEN 'unread'
|
|
54
71
|
ELSE 'read'
|
|
@@ -89,6 +106,17 @@ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
|
|
|
89
106
|
const sources = opts.source?.filter(Boolean) ?? [];
|
|
90
107
|
if (sources.length)
|
|
91
108
|
add({ sql: `r.source IN (${sources.map(() => "?").join(",")})`, params: sources });
|
|
109
|
+
const hidden = opts.hideSource?.filter(Boolean) ?? [];
|
|
110
|
+
if (hidden.length)
|
|
111
|
+
add({ sql: `r.source NOT IN (${hidden.map(() => "?").join(",")})`, params: hidden });
|
|
112
|
+
if (opts.showSource) {
|
|
113
|
+
const shown = opts.showSource.filter(Boolean);
|
|
114
|
+
add(
|
|
115
|
+
shown.length
|
|
116
|
+
? { sql: `r.source IN (${shown.map(() => "?").join(",")})`, params: shown }
|
|
117
|
+
: { sql: "0", params: [] },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
92
120
|
if (opts.q?.trim()) {
|
|
93
121
|
const like = `%${opts.q.trim().toLowerCase()}%`;
|
|
94
122
|
add({
|
|
@@ -106,6 +134,7 @@ export function sessionRootFilter(opts: SessionFilters = {}): SqlFragment {
|
|
|
106
134
|
if (opts.viewer) {
|
|
107
135
|
add(sessionViewerCondition(opts.viewer));
|
|
108
136
|
if (opts.tree) conditions.push("r.requestedByUserId IS NOT NULL");
|
|
137
|
+
if (opts.mine) add(sessionMineCondition(opts.viewer.userId));
|
|
109
138
|
}
|
|
110
139
|
if (opts.keyPrefix) {
|
|
111
140
|
const { lo, hi } = keyPrefixRange(opts.keyPrefix);
|
|
@@ -140,6 +169,35 @@ function hasSessionChainFilter(opts: SessionFilters): boolean {
|
|
|
140
169
|
|
|
141
170
|
const runningSql = `latestStatus NOT IN (${TERMINAL_TASK_STATUSES.map((s) => `'${s}'`).join(",")})`;
|
|
142
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Sources of tasks that agents and the server create: delegation and scripts
|
|
174
|
+
* (`mcp`), follow-ups (`system`), deferred wake-ups (`schedule`) and workflow
|
|
175
|
+
* steps. Agents also set `requestedByUserId` explicitly on these, so a requester
|
|
176
|
+
* alone does not mark a human message.
|
|
177
|
+
*/
|
|
178
|
+
const AGENT_TASK_SOURCES = ["mcp", "system", "schedule", "workflow"] as const;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Per chain row, over `chain` and the viewer's Done row `d`: whether the row
|
|
182
|
+
* needs the user since they marked the session Done. That is a human message
|
|
183
|
+
* (a task attributed to a person directly, not copied from its parent, from a
|
|
184
|
+
* human channel), a person's steering message on a chain task (steer or
|
|
185
|
+
* queue, not cancelled), or a pending approval, created after the Done mark.
|
|
186
|
+
* Steering `created_at` and the Done mark's `lastUpdatedAt` are both written
|
|
187
|
+
* as `%Y-%m-%dT%H:%M:%fZ`, so they compare as text.
|
|
188
|
+
*/
|
|
189
|
+
const reopensDoneSql = `(d.id IS NOT NULL AND (
|
|
190
|
+
(chain.createdAt > d.lastUpdatedAt
|
|
191
|
+
AND chain.requestedByUserId IS NOT NULL
|
|
192
|
+
AND chain.requestedByUserIdInherited = 0
|
|
193
|
+
AND COALESCE(chain.source, '') NOT IN (${AGENT_TASK_SOURCES.map((s) => `'${s}'`).join(",")}))
|
|
194
|
+
OR EXISTS (SELECT 1 FROM task_steering_messages sm
|
|
195
|
+
WHERE sm.task_id = chain.id AND sm.created_by_kind = 'user'
|
|
196
|
+
AND sm.status != 'cancelled' AND sm.created_at > d.lastUpdatedAt)
|
|
197
|
+
OR EXISTS (SELECT 1 FROM approval_requests ar
|
|
198
|
+
WHERE ar.sourceTaskId = chain.id AND ar.status = 'pending' AND ar.createdAt > d.lastUpdatedAt)
|
|
199
|
+
))`;
|
|
200
|
+
|
|
143
201
|
/**
|
|
144
202
|
* A session starts at a parentTaskId IS NULL root. One recursive CTE computes
|
|
145
203
|
* chain-wide latest activity for list ordering. Root filters precede recursion.
|
|
@@ -150,7 +208,7 @@ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
|
|
|
150
208
|
const read = opts.viewer?.userId ? sessionReadStateSql(opts.now) : { sql: "NULL", params: [] };
|
|
151
209
|
const params = [...root.params];
|
|
152
210
|
if (opts.withUserId) params.push(opts.withUserId);
|
|
153
|
-
params.push(...read.params, opts.viewer?.userId ?? null);
|
|
211
|
+
params.push(opts.viewer?.userId ?? null, ...read.params, opts.viewer?.userId ?? null);
|
|
154
212
|
const filters: string[] = [];
|
|
155
213
|
if (opts.triage === "unread") filters.push("readState = 'unread'");
|
|
156
214
|
else if (opts.triage === "later" || opts.triage === "done") {
|
|
@@ -170,19 +228,27 @@ export function sessionSummaryQuery(opts: SessionFilters = {}): SqlFragment {
|
|
|
170
228
|
return {
|
|
171
229
|
// SQLite takes the bare status from the row with MAX(lastUpdatedAt).
|
|
172
230
|
// Keep this as the only MIN/MAX aggregate so status follows activity.
|
|
173
|
-
|
|
174
|
-
|
|
231
|
+
// `d` is the viewer's Done row, so `reopensDone` can compare chain rows
|
|
232
|
+
// against the Done mark (SUM keeps MAX the only MIN/MAX aggregate).
|
|
233
|
+
sql: `WITH RECURSIVE chain(root_id, id, lastUpdatedAt, status, requestedByUserId,
|
|
234
|
+
createdAt, source, requestedByUserIdInherited) AS (
|
|
235
|
+
SELECT r.id, r.id, r.lastUpdatedAt, r.status, r.requestedByUserId,
|
|
236
|
+
r.createdAt, r.source, r.requestedByUserIdInherited
|
|
175
237
|
FROM agent_tasks r WHERE ${root.sql}
|
|
176
238
|
UNION ALL
|
|
177
|
-
SELECT c.root_id, t.id, t.lastUpdatedAt, t.status, t.requestedByUserId
|
|
239
|
+
SELECT c.root_id, t.id, t.lastUpdatedAt, t.status, t.requestedByUserId,
|
|
240
|
+
t.createdAt, t.source, t.requestedByUserIdInherited
|
|
178
241
|
FROM chain c JOIN agent_tasks t ON t.parentTaskId = c.id
|
|
179
242
|
), agg AS (
|
|
180
|
-
SELECT root_id, COUNT(*) AS chainCount,
|
|
181
|
-
status AS latestStatus, MAX(lastUpdatedAt) AS lastActivityAt,
|
|
182
|
-
${opts.withUserId ? "SUM(requestedByUserId = ?) AS withUser," : ""}
|
|
243
|
+
SELECT chain.root_id, COUNT(*) AS chainCount,
|
|
244
|
+
chain.status AS latestStatus, MAX(chain.lastUpdatedAt) AS lastActivityAt,
|
|
245
|
+
${opts.withUserId ? "SUM(chain.requestedByUserId = ?) AS withUser," : ""}
|
|
183
246
|
SUM(EXISTS (SELECT 1 FROM approval_requests ar
|
|
184
|
-
WHERE ar.sourceTaskId = chain.id AND ar.status = 'pending')) AS pendingApproval
|
|
185
|
-
|
|
247
|
+
WHERE ar.sourceTaskId = chain.id AND ar.status = 'pending')) AS pendingApproval,
|
|
248
|
+
SUM(${reopensDoneSql}) AS reopensDone
|
|
249
|
+
FROM chain LEFT JOIN inbox_item_state d
|
|
250
|
+
ON d.userId = ? AND d.itemType = 'to_read' AND d.itemId = chain.root_id AND d.status = 'done'
|
|
251
|
+
GROUP BY chain.root_id
|
|
186
252
|
), state AS (
|
|
187
253
|
SELECT a.*, ${read.sql} AS readState, s.snoozeUntil
|
|
188
254
|
FROM agg a LEFT JOIN inbox_item_state s
|
|
@@ -322,25 +388,32 @@ export interface SessionTree {
|
|
|
322
388
|
later: SessionSectionCounts;
|
|
323
389
|
done: SessionSectionCounts;
|
|
324
390
|
noGroup: SessionSectionCounts;
|
|
391
|
+
/** Each root source in this view with its session count, hidden sources included, most first. */
|
|
392
|
+
sources: { source: string; total: number }[];
|
|
325
393
|
}
|
|
326
394
|
|
|
327
395
|
export async function getSessionTree(
|
|
328
396
|
actor: { kind: "bypass" } | { kind: "user"; userId: string | null },
|
|
329
397
|
now?: string,
|
|
398
|
+
opts: { mine?: boolean; hideSource?: string[]; showSource?: string[] } = {},
|
|
330
399
|
): Promise<SessionTree> {
|
|
400
|
+
// No source filter in SQL: the source counts must include hidden sources,
|
|
401
|
+
// so the filter menu can offer them back. The loop skips them instead.
|
|
331
402
|
const query = sessionSummaryQuery({
|
|
332
403
|
...(actor.kind === "user" ? { viewer: { userId: actor.userId, scope: "all" } as const } : {}),
|
|
333
404
|
now,
|
|
334
405
|
tree: true,
|
|
406
|
+
mine: opts.mine,
|
|
335
407
|
});
|
|
336
408
|
const rows = await getDbClient().query<{
|
|
337
409
|
key: string;
|
|
410
|
+
source: string;
|
|
338
411
|
lastActivityAt: string;
|
|
339
412
|
latestStatus: string;
|
|
340
413
|
readState: SessionReadState | null;
|
|
341
414
|
needsYou: number;
|
|
342
415
|
}>(
|
|
343
|
-
`SELECT r."key", a.lastActivityAt, a.latestStatus, a.readState, a.needsYou
|
|
416
|
+
`SELECT r."key", r.source, a.lastActivityAt, a.latestStatus, a.readState, a.needsYou
|
|
344
417
|
FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id`,
|
|
345
418
|
query.params,
|
|
346
419
|
);
|
|
@@ -351,7 +424,11 @@ export async function getSessionTree(
|
|
|
351
424
|
later: counts(),
|
|
352
425
|
done: counts(),
|
|
353
426
|
noGroup: counts(),
|
|
427
|
+
sources: [],
|
|
354
428
|
};
|
|
429
|
+
const hidden = new Set(opts.hideSource);
|
|
430
|
+
const shown = opts.showSource ? new Set(opts.showSource) : null;
|
|
431
|
+
const sources = new Map<string, number>();
|
|
355
432
|
const groups = new Map<string, SessionTreeGroup>();
|
|
356
433
|
const add = (section: SessionSectionCounts, row: (typeof rows)[number]) => {
|
|
357
434
|
section.total++;
|
|
@@ -360,6 +437,8 @@ export async function getSessionTree(
|
|
|
360
437
|
if (row.needsYou) section.needsYou++;
|
|
361
438
|
};
|
|
362
439
|
for (const row of rows) {
|
|
440
|
+
sources.set(row.source, (sources.get(row.source) ?? 0) + 1);
|
|
441
|
+
if (hidden.has(row.source) || (shown && !shown.has(row.source))) continue;
|
|
363
442
|
const key = sessionGroupKey(row.key);
|
|
364
443
|
const noGroup =
|
|
365
444
|
key === "shared/" || (actor.kind === "user" && key === `personal/${actor.userId}/`);
|
|
@@ -380,5 +459,71 @@ export async function getSessionTree(
|
|
|
380
459
|
}
|
|
381
460
|
}
|
|
382
461
|
result.groups = [...groups.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
462
|
+
result.sources = [...sources]
|
|
463
|
+
.map(([source, total]) => ({ source, total }))
|
|
464
|
+
.sort((a, b) => b.total - a.total || a.source.localeCompare(b.source));
|
|
383
465
|
return result;
|
|
384
466
|
}
|
|
467
|
+
|
|
468
|
+
/** Triage actions on a whole list: the bulk bar's "Select all N". */
|
|
469
|
+
export type SessionListTriage = "done" | "inbox" | "read" | "unread";
|
|
470
|
+
|
|
471
|
+
/** The read cursor of "unread". The dashboard and the my-swarm app write the same value. */
|
|
472
|
+
const UNREAD_CURSOR = "1970-01-01T00:00:00.000Z";
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Apply one triage action to every session a sidebar list shows `userId`
|
|
476
|
+
* (the same filters as `GET /api/sessions?scope=tree`), in one statement.
|
|
477
|
+
* The rows match what the dashboard writes for one session: Done, back to
|
|
478
|
+
* the Inbox, read up to the newest activity, or unread. Returns the number
|
|
479
|
+
* of sessions written.
|
|
480
|
+
*/
|
|
481
|
+
export async function triageSessionList(
|
|
482
|
+
userId: string,
|
|
483
|
+
filters: Pick<
|
|
484
|
+
SessionFilters,
|
|
485
|
+
"triage" | "noGroup" | "groupKey" | "mine" | "hideSource" | "showSource" | "now"
|
|
486
|
+
>,
|
|
487
|
+
action: SessionListTriage,
|
|
488
|
+
): Promise<number> {
|
|
489
|
+
const now = filters.now ?? new Date().toISOString();
|
|
490
|
+
const query = sessionSummaryQuery({
|
|
491
|
+
...filters,
|
|
492
|
+
now,
|
|
493
|
+
viewer: { userId, scope: "all" },
|
|
494
|
+
tree: true,
|
|
495
|
+
});
|
|
496
|
+
const status = action === "done" ? "done" : "open";
|
|
497
|
+
// `read` moves the cursor to each session's newest activity, `unread` to the epoch.
|
|
498
|
+
const lastReadAt =
|
|
499
|
+
action === "read" ? "a.lastActivityAt" : action === "unread" ? `'${UNREAD_CURSOR}'` : "NULL";
|
|
500
|
+
const setsLastRead = action === "read" || action === "unread";
|
|
501
|
+
// The conflict rules copy `upsertInboxState` in src/be/db.ts (sticky readAt,
|
|
502
|
+
// lastReadAt only when the action sets it, snooze and dismiss cleared). Keep
|
|
503
|
+
// the two in step. `WHERE true` is required: SQLite cannot parse
|
|
504
|
+
// INSERT ... SELECT ... ON CONFLICT when the SELECT has no WHERE clause.
|
|
505
|
+
const result = await getDbClient().run(
|
|
506
|
+
`INSERT INTO inbox_item_state
|
|
507
|
+
(userId, itemType, itemId, status, snoozeUntil, dismissedAt, doneAt, readAt, lastReadAt, createdAt, lastUpdatedAt)
|
|
508
|
+
SELECT ?, 'to_read', a.root_id, ?, NULL, NULL, ?, ?, ${lastReadAt}, ?, ?
|
|
509
|
+
FROM (${query.sql}) a WHERE true
|
|
510
|
+
ON CONFLICT(userId, itemType, itemId) DO UPDATE SET
|
|
511
|
+
status = excluded.status,
|
|
512
|
+
snoozeUntil = NULL,
|
|
513
|
+
dismissedAt = NULL,
|
|
514
|
+
doneAt = excluded.doneAt,
|
|
515
|
+
readAt = COALESCE(inbox_item_state.readAt, excluded.readAt),
|
|
516
|
+
lastReadAt = ${setsLastRead ? "excluded.lastReadAt" : "inbox_item_state.lastReadAt"},
|
|
517
|
+
lastUpdatedAt = excluded.lastUpdatedAt`,
|
|
518
|
+
[
|
|
519
|
+
userId,
|
|
520
|
+
status,
|
|
521
|
+
action === "done" ? now : null,
|
|
522
|
+
action === "read" ? now : null,
|
|
523
|
+
now,
|
|
524
|
+
now,
|
|
525
|
+
...query.params,
|
|
526
|
+
],
|
|
527
|
+
);
|
|
528
|
+
return result.changes;
|
|
529
|
+
}
|
|
@@ -27,3 +27,34 @@ export function sessionViewerCondition(viewer: SessionViewer): {
|
|
|
27
27
|
params: [viewer.userId, viewer.userId],
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Root sources that a machine starts: a workflow run, a schedule, or the
|
|
33
|
+
* swarm itself (heartbeat, follow-ups, budget and approval notices). A
|
|
34
|
+
* workflow root carries the user whose trigger fired it, so the requester
|
|
35
|
+
* alone does not tell a person's session from fan-out. `kapso-inbound` is the
|
|
36
|
+
* exception: a WhatsApp message a person sent, written with `system`.
|
|
37
|
+
*/
|
|
38
|
+
export const MACHINE_SESSION_SOURCES = ["workflow", "schedule", "system"] as const;
|
|
39
|
+
const HUMAN_SYSTEM_TASK_TYPES = ["kapso-inbound"] as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The sessions v2 "Mine" view (alias `r`): roots this user started from a
|
|
43
|
+
* human channel (every source outside {@link MACHINE_SESSION_SOURCES}), plus
|
|
44
|
+
* sessions shared with this user or everyone. A list filter only: it narrows
|
|
45
|
+
* what a list shows and never changes who can open a session.
|
|
46
|
+
*/
|
|
47
|
+
export function sessionMineCondition(userId: string | null): {
|
|
48
|
+
sql: string;
|
|
49
|
+
params: (string | null)[];
|
|
50
|
+
} {
|
|
51
|
+
const machine = MACHINE_SESSION_SOURCES.map(() => "?").join(",");
|
|
52
|
+
const humanSystem = HUMAN_SYSTEM_TASK_TYPES.map(() => "?").join(",");
|
|
53
|
+
return {
|
|
54
|
+
sql: `((r.requestedByUserId = ? AND (r.source NOT IN (${machine})
|
|
55
|
+
OR (r.source = 'system' AND r.taskType IN (${humanSystem}))))
|
|
56
|
+
OR EXISTS (SELECT 1 FROM session_shares s
|
|
57
|
+
WHERE s.rootTaskId = r.id AND s.revokedAt IS NULL AND (s.userId IS NULL OR s.userId = ?)))`,
|
|
58
|
+
params: [userId, ...MACHINE_SESSION_SOURCES, ...HUMAN_SYSTEM_TASK_TYPES, userId],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getDbClient } from "./runtime";
|
|
2
|
+
|
|
3
|
+
// bm25() takes one weight per FTS column, in declaration order.
|
|
4
|
+
// skills_fts (migration 239): skill_id (UNINDEXED), name, description, content.
|
|
5
|
+
// skills_meta_fts (migration 241): skill_id (UNINDEXED), name, description.
|
|
6
|
+
const SKILLS_FTS_BM25 = "bm25(skills_fts, 0.0, 3.0, 2.0, 1.0)";
|
|
7
|
+
const SKILLS_META_FTS_BM25 = "bm25(skills_meta_fts, 0.0, 3.0, 2.0)";
|
|
8
|
+
|
|
9
|
+
// FTS5 bm25 normalizes by the length of the whole row, so in skills_fts a
|
|
10
|
+
// long SKILL.md body buries a name or description hit. skills_meta_fts holds
|
|
11
|
+
// only those short fields; its score is added at this multiple. On the live
|
|
12
|
+
// catalog, multiples from 2 to 10 rank the same; 3 is mid-range.
|
|
13
|
+
const META_SCORE_MULTIPLE = 3;
|
|
14
|
+
|
|
15
|
+
// English function words that agents put in natural-language queries. Each
|
|
16
|
+
// one matches most of the catalog ("a" matched 189 of 189 skills) and only
|
|
17
|
+
// adds noise to the ranking.
|
|
18
|
+
const STOPWORDS = new Set(
|
|
19
|
+
(
|
|
20
|
+
"a an and are as at be but by can do does for from get got have how i if in into is it its " +
|
|
21
|
+
"me my need of on or our should so that the their them then there these this to use using " +
|
|
22
|
+
"want was we what when where which who why will with would you your"
|
|
23
|
+
).split(" "),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a skills_fts MATCH expression from free text. Every term is quoted as
|
|
28
|
+
* an FTS5 string, so user text cannot inject operators (NEAR, AND, -, *, ^,
|
|
29
|
+
* column filters). Stopwords are dropped and the remaining terms are OR-ed:
|
|
30
|
+
* ranked recall, ordered by bm25. Returns null when no term is left, so the
|
|
31
|
+
* caller falls back to the substring match.
|
|
32
|
+
*/
|
|
33
|
+
export function buildSkillsFtsMatch(query: string): string | null {
|
|
34
|
+
const terms = query
|
|
35
|
+
.split(/[^\p{L}\p{N}_-]+/u)
|
|
36
|
+
.filter((term) => /[\p{L}\p{N}]/u.test(term) && !STOPWORDS.has(term.toLowerCase()))
|
|
37
|
+
.slice(0, 12);
|
|
38
|
+
if (terms.length === 0) return null;
|
|
39
|
+
return terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Enabled skills ranked by BM25: the skills_fts score (name 3, description 2,
|
|
44
|
+
* content 1) plus META_SCORE_MULTIPLE times the skills_meta_fts score (name 3,
|
|
45
|
+
* description 2). A skill whose name equals the whole query (case-insensitive)
|
|
46
|
+
* ranks first. Returns [] when the query has no indexable terms or the
|
|
47
|
+
* backend cannot run the FTS query, so the caller can fall back.
|
|
48
|
+
*
|
|
49
|
+
* `columns` is a select list over `skills`; it must not name a bare
|
|
50
|
+
* `skill_id` or `score`, which the joined `hits` and `meta` CTEs also expose.
|
|
51
|
+
*/
|
|
52
|
+
export async function searchSkillRowsRanked<T>(
|
|
53
|
+
query: string,
|
|
54
|
+
limit: number,
|
|
55
|
+
columns: string,
|
|
56
|
+
): Promise<T[]> {
|
|
57
|
+
const match = buildSkillsFtsMatch(query);
|
|
58
|
+
if (!match) return [];
|
|
59
|
+
const exactName = query.trim();
|
|
60
|
+
try {
|
|
61
|
+
return await getDbClient().query<T>(
|
|
62
|
+
`WITH hits AS (
|
|
63
|
+
SELECT skill_id, ${SKILLS_FTS_BM25} AS score FROM skills_fts WHERE skills_fts MATCH ?
|
|
64
|
+
),
|
|
65
|
+
meta AS (
|
|
66
|
+
SELECT skill_id, ${SKILLS_META_FTS_BM25} AS score
|
|
67
|
+
FROM skills_meta_fts WHERE skills_meta_fts MATCH ?
|
|
68
|
+
)
|
|
69
|
+
SELECT ${columns} FROM skills
|
|
70
|
+
LEFT JOIN hits ON hits.skill_id = skills.id
|
|
71
|
+
LEFT JOIN meta ON meta.skill_id = skills.id
|
|
72
|
+
WHERE skills.isEnabled = 1
|
|
73
|
+
AND (hits.skill_id IS NOT NULL OR lower(skills.name) = lower(?))
|
|
74
|
+
ORDER BY lower(skills.name) = lower(?) DESC,
|
|
75
|
+
COALESCE(hits.score, 0) + ${META_SCORE_MULTIPLE} * COALESCE(meta.score, 0) ASC,
|
|
76
|
+
skills.name ASC
|
|
77
|
+
LIMIT ?`,
|
|
78
|
+
[match, match, exactName, exactName, limit],
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn(
|
|
82
|
+
"[skills-fts] ranked query failed, using substring match:",
|
|
83
|
+
(err as Error).message,
|
|
84
|
+
);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/be/db/tasks/read.ts
CHANGED
|
@@ -196,12 +196,39 @@ type AgentTaskTimelineRow = Pick<
|
|
|
196
196
|
* past it).
|
|
197
197
|
*/
|
|
198
198
|
const TASK_PREVIEW_BYTES = 4 * (TASK_PREVIEW_LENGTH + 1);
|
|
199
|
-
|
|
199
|
+
/**
|
|
200
|
+
* A leading `<sibling_tasks_in_progress>` block (`src/tasks/sibling-block.ts`)
|
|
201
|
+
* is generated context that can fill the whole preview, so the list row never
|
|
202
|
+
* reaches the ask. The preview starts after the block and the `\n\n`
|
|
203
|
+
* `prependSiblingBlock` puts after it. A block with no closing tag stays.
|
|
204
|
+
*/
|
|
205
|
+
const SIBLING_OPEN = "<sibling_tasks_in_progress>";
|
|
206
|
+
const SIBLING_CLOSE = "</sibling_tasks_in_progress>";
|
|
207
|
+
const TASK_BLOB = "CAST(agent_tasks.task AS BLOB)";
|
|
208
|
+
const SIBLING_CLOSE_AT = `instr(${TASK_BLOB}, CAST('${SIBLING_CLOSE}' AS BLOB))`;
|
|
209
|
+
const SIBLING_END = `(${SIBLING_CLOSE_AT} + ${SIBLING_CLOSE.length})`;
|
|
210
|
+
const TASK_PREVIEW_START = `CASE
|
|
211
|
+
WHEN substr(${TASK_BLOB}, 1, ${SIBLING_OPEN.length}) = CAST('${SIBLING_OPEN}' AS BLOB)
|
|
212
|
+
AND ${SIBLING_CLOSE_AT} > 0
|
|
213
|
+
THEN ${SIBLING_END} + (substr(${TASK_BLOB}, ${SIBLING_END}, 2) = x'0a0a') * 2
|
|
214
|
+
ELSE 1 END`;
|
|
215
|
+
const TASK_PREVIEW_SQL = `substr(${TASK_BLOB}, ${TASK_PREVIEW_START}, ${TASK_PREVIEW_BYTES}) AS task`;
|
|
200
216
|
// ignoreBOM keeps a leading U+FEFF, which the full-text preview keeps too.
|
|
201
217
|
const taskPreviewDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
202
218
|
|
|
219
|
+
/** What `TASK_PREVIEW_START` does in SQL, for a full-text row. */
|
|
220
|
+
function skipSiblingBlock(text: string): string {
|
|
221
|
+
if (!text.startsWith(SIBLING_OPEN)) return text;
|
|
222
|
+
const close = text.indexOf(SIBLING_CLOSE);
|
|
223
|
+
if (close === -1) return text;
|
|
224
|
+
const end = close + SIBLING_CLOSE.length;
|
|
225
|
+
return text.slice(text.startsWith("\n\n", end) ? end + 2 : end);
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
function taskPreview(task: TaskPreviewSource["task"]): string {
|
|
204
|
-
|
|
229
|
+
// Bytes come from `TASK_PREVIEW_SQL`, which already skipped the block.
|
|
230
|
+
const text =
|
|
231
|
+
task instanceof Uint8Array ? taskPreviewDecoder.decode(task) : skipSiblingBlock(task ?? "");
|
|
205
232
|
return dependencies.previewText(text, TASK_PREVIEW_LENGTH);
|
|
206
233
|
}
|
|
207
234
|
|
|
@@ -1715,6 +1742,18 @@ export async function getLatestLeadTaskInThread(
|
|
|
1715
1742
|
return row ? rowToAgentTask(row) : null;
|
|
1716
1743
|
}
|
|
1717
1744
|
|
|
1745
|
+
/**
|
|
1746
|
+
* `source` filter of the contextKey lookups below. `'chat'` also matches a
|
|
1747
|
+
* defer-task wake-up of a chat task: it keeps the chat contextKey with
|
|
1748
|
+
* `source = 'schedule'` (src/scheduler/schedule-task.ts) and continues the
|
|
1749
|
+
* same ask, so `/cancel`, steering and routing treat it as the chat task.
|
|
1750
|
+
* Binds `source` three times.
|
|
1751
|
+
*/
|
|
1752
|
+
function contextKeySourceSql(alias = ""): string {
|
|
1753
|
+
return `(? IS NULL OR ${alias}source = ?
|
|
1754
|
+
OR (? = 'chat' AND ${alias}source = 'schedule' AND ${alias}taskType = 'deferred'))`;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1718
1757
|
/**
|
|
1719
1758
|
* ContextKey twins of the Slack thread lookups above, for channels that key a
|
|
1720
1759
|
* conversation by `contextKey` only (Chat SDK). `source` narrows to the
|
|
@@ -1731,7 +1770,7 @@ export async function getAgentWorkingOnContextKey(
|
|
|
1731
1770
|
const taskRow = await getDbClient().get<AgentTaskRow>(
|
|
1732
1771
|
`SELECT agentId FROM agent_tasks
|
|
1733
1772
|
WHERE contextKey = ?
|
|
1734
|
-
AND (
|
|
1773
|
+
AND ${contextKeySourceSql()}
|
|
1735
1774
|
AND (? IS NULL
|
|
1736
1775
|
OR status NOT IN (${TERMINAL_TASK_STATUSES.map(() => "?").join(", ")})
|
|
1737
1776
|
OR finishedAt >= ?)
|
|
@@ -1741,6 +1780,7 @@ export async function getAgentWorkingOnContextKey(
|
|
|
1741
1780
|
contextKey,
|
|
1742
1781
|
source ?? null,
|
|
1743
1782
|
source ?? null,
|
|
1783
|
+
source ?? null,
|
|
1744
1784
|
endedSince ?? null,
|
|
1745
1785
|
...TERMINAL_TASK_STATUSES,
|
|
1746
1786
|
endedSince ?? null,
|
|
@@ -1760,11 +1800,11 @@ export async function getLatestActiveTaskByContextKey(
|
|
|
1760
1800
|
const row = await getDbClient().get<AgentTaskRow>(
|
|
1761
1801
|
`SELECT * FROM agent_tasks
|
|
1762
1802
|
WHERE contextKey = ?
|
|
1763
|
-
AND (
|
|
1803
|
+
AND ${contextKeySourceSql()}
|
|
1764
1804
|
AND status IN ('in_progress', 'pending')
|
|
1765
1805
|
ORDER BY createdAt DESC, rowid DESC
|
|
1766
1806
|
LIMIT 1`,
|
|
1767
|
-
[contextKey, source ?? null, source ?? null],
|
|
1807
|
+
[contextKey, source ?? null, source ?? null, source ?? null],
|
|
1768
1808
|
);
|
|
1769
1809
|
|
|
1770
1810
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -1780,11 +1820,11 @@ export async function getLatestLeadTaskByContextKey(
|
|
|
1780
1820
|
FROM agent_tasks t
|
|
1781
1821
|
JOIN agents a ON a.id = t.agentId
|
|
1782
1822
|
WHERE t.contextKey = ?
|
|
1783
|
-
AND (
|
|
1823
|
+
AND ${contextKeySourceSql("t.")}
|
|
1784
1824
|
AND a.isLead = 1
|
|
1785
1825
|
ORDER BY t.createdAt DESC, t.rowid DESC
|
|
1786
1826
|
LIMIT 1`,
|
|
1787
|
-
[contextKey, source ?? null, source ?? null],
|
|
1827
|
+
[contextKey, source ?? null, source ?? null, source ?? null],
|
|
1788
1828
|
);
|
|
1789
1829
|
|
|
1790
1830
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -4,7 +4,8 @@ import { scrubJsonValue } from "../scrub-json";
|
|
|
4
4
|
import { getDbClient } from "./runtime";
|
|
5
5
|
|
|
6
6
|
// Parent/child links between workflow runs (migration 203): a `sub-workflow`
|
|
7
|
-
// step starts a child run that records the step in `parentStepId
|
|
7
|
+
// step starts a child run that records the step in `parentStepId`, and the
|
|
8
|
+
// step's `retryCount` at that time in `parentStepAttempt` (migration 240).
|
|
8
9
|
|
|
9
10
|
const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
|
|
10
11
|
"completed",
|
|
@@ -13,15 +14,60 @@ const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
|
|
|
13
14
|
"skipped",
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
|
-
/** The id of the child run a `sub-workflow` step started
|
|
17
|
-
export async function getChildWorkflowRunId(
|
|
17
|
+
/** The id of the child run a `sub-workflow` step started on `attempt`, if any. */
|
|
18
|
+
export async function getChildWorkflowRunId(
|
|
19
|
+
parentStepId: string,
|
|
20
|
+
attempt: number,
|
|
21
|
+
): Promise<string | null> {
|
|
18
22
|
const row = await getDbClient().get<{ id: string }>(
|
|
19
|
-
"SELECT id FROM workflow_runs WHERE parentStepId = ?",
|
|
20
|
-
[parentStepId],
|
|
23
|
+
"SELECT id FROM workflow_runs WHERE parentStepId = ? AND parentStepAttempt = ?",
|
|
24
|
+
[parentStepId, attempt],
|
|
21
25
|
);
|
|
22
26
|
return row?.id ?? null;
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* True while `attempt` of the step is executing in a live run: the step is
|
|
31
|
+
* `running` with that `retryCount`. A child run is created only then, so a
|
|
32
|
+
* cancelled run, a step another attempt moved on, or a step already parked
|
|
33
|
+
* never gets a new child.
|
|
34
|
+
*/
|
|
35
|
+
export async function isParentStepAttemptCurrent(
|
|
36
|
+
parentStepId: string,
|
|
37
|
+
attempt: number,
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
const row = await getDbClient().get<{ found: number }>(
|
|
40
|
+
`SELECT 1 AS found
|
|
41
|
+
FROM workflow_run_steps s
|
|
42
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
43
|
+
WHERE s.id = ? AND s.status = 'running' AND s.retryCount = ?
|
|
44
|
+
AND r.status IN ('running', 'waiting')`,
|
|
45
|
+
[parentStepId, attempt],
|
|
46
|
+
);
|
|
47
|
+
return row != null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `sub-workflow` steps left `running` in a live run: each is its node's latest
|
|
52
|
+
* row, so a row a newer one superseded is not listed. The caller skips the
|
|
53
|
+
* ones this process is still executing.
|
|
54
|
+
*/
|
|
55
|
+
export async function getRunningSubWorkflowSteps(): Promise<
|
|
56
|
+
Array<{ stepId: string; runId: string; nodeId: string }>
|
|
57
|
+
> {
|
|
58
|
+
return getDbClient().query<{ stepId: string; runId: string; nodeId: string }>(
|
|
59
|
+
`SELECT s.id AS stepId, s.runId AS runId, s.nodeId AS nodeId
|
|
60
|
+
FROM workflow_run_steps s
|
|
61
|
+
JOIN workflow_runs r ON r.id = s.runId
|
|
62
|
+
WHERE s.status = 'running' AND s.nodeType = 'sub-workflow'
|
|
63
|
+
AND r.status IN ('running', 'waiting')
|
|
64
|
+
AND s.rowid = (
|
|
65
|
+
SELECT MAX(rowid) FROM workflow_run_steps
|
|
66
|
+
WHERE runId = s.runId AND nodeId = s.nodeId
|
|
67
|
+
)`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
/** Where a child run came from: the `sub-workflow` step that started it and that step's run. */
|
|
26
72
|
export type WorkflowRunParent = {
|
|
27
73
|
runId: string;
|
|
@@ -65,12 +111,13 @@ export async function getWorkflowRunLineage(runId: string, maxDepth: number): Pr
|
|
|
65
111
|
return rows.map((row) => row.workflowId);
|
|
66
112
|
}
|
|
67
113
|
|
|
68
|
-
/** Waiting `sub-workflow` steps whose child run is terminal or deleted. */
|
|
114
|
+
/** Waiting `sub-workflow` steps whose current attempt's child run is terminal or deleted. */
|
|
69
115
|
export async function getSettledChildRunParentSteps(): Promise<string[]> {
|
|
70
116
|
const rows = await getDbClient().query<{ id: string }>(
|
|
71
117
|
`SELECT s.id
|
|
72
118
|
FROM workflow_run_steps s
|
|
73
|
-
LEFT JOIN workflow_runs c
|
|
119
|
+
LEFT JOIN workflow_runs c
|
|
120
|
+
ON c.parentStepId = s.id AND c.parentStepAttempt = s.retryCount
|
|
74
121
|
WHERE s.status = 'waiting' AND s.nodeType = 'sub-workflow'
|
|
75
122
|
AND (c.id IS NULL OR c.status IN ('completed', 'failed', 'cancelled', 'skipped'))`,
|
|
76
123
|
);
|