@desplega.ai/agent-swarm 2.7.2 → 2.7.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/README.md +6 -0
- package/dist/{acp-adapter-624yge7a.js → acp-adapter-pdzw76ja.js} +1 -1
- package/dist/{actions-rd9xn281.js → actions-a90cfpca.js} +21 -21
- package/dist/{amp-adapter-k27hc00p.js → amp-adapter-4vtywcjk.js} +2 -2
- package/dist/{app-sjm3xc7g.js → app-8k68gygk.js} +3 -3
- package/dist/{assistant-6jbzs8wa.js → assistant-ck01q2yg.js} +32 -21
- package/dist/{boot-reembed-e2348eqc.js → boot-reembed-33spavpd.js} +1 -1
- package/dist/{boot-reembed-w53pd5xt.js → boot-reembed-xzge83a2.js} +1 -1
- package/dist/{boot-scrub-sweep-cpy4yg97.js → boot-scrub-sweep-mxvb8dr3.js} +3 -3
- package/dist/{boot-seeds-f6j4t6k6.js → boot-seeds-rpm8bb5r.js} +4 -4
- package/dist/{claude-adapter-v3ac8esd.js → claude-adapter-q12bcfj9.js} +3 -3
- package/dist/{claude-managed-setup-yyrkqvtm.js → claude-managed-setup-11vb6q89.js} +1 -1
- package/dist/{claude-sdk-session-85j69v6d.js → claude-sdk-session-gkv1107j.js} +10325 -10243
- package/dist/{cli-rs59z1qx.js → cli-07pcqwaj.js} +1 -1
- package/dist/{cli-6vq3ymqm.js → cli-0tkm519n.js} +1 -1
- package/dist/{cli-s7cm1927.js → cli-0xzbk52h.js} +4 -4
- package/dist/{cli-g0zc1b40.js → cli-0zae6bs6.js} +2 -2
- package/dist/{cli-p8yrsmfn.js → cli-10kjx0dm.js} +1 -1
- package/dist/{cli-7xsq63pe.js → cli-157ykn37.js} +10 -9
- package/dist/{cli-vjcj9k2p.js → cli-2h0rfmqk.js} +1 -1
- package/dist/{cli-9wasax9f.js → cli-35nzvqaf.js} +1 -1
- package/dist/{cli-e1p7dqe1.js → cli-4aydmmdy.js} +24 -3
- package/dist/{cli-942pp3bc.js → cli-54ax65rn.js} +3 -3
- package/dist/{cli-10cp92df.js → cli-5ke7gxwr.js} +6 -6
- package/dist/{cli-femw4q81.js → cli-6151a7dr.js} +2 -2
- package/dist/{cli-3cywb44q.js → cli-77eq8txs.js} +85 -3
- package/dist/{cli-atpfk4yd.js → cli-7cwrwb5x.js} +5 -5
- package/dist/{cli-bfbpvccx.js → cli-8a1k6mat.js} +61 -49
- package/dist/{cli-c18941tx.js → cli-8ch3b2r7.js} +3 -3
- package/dist/{cli-rs2s1t05.js → cli-9fk56y8c.js} +4 -4
- package/dist/{cli-0wx63vvw.js → cli-9km88w11.js} +1 -1
- package/dist/{cli-qy0z3mfj.js → cli-9vttw9nz.js} +56 -46
- package/dist/{cli-mzgppb7q.js → cli-a0w50zw9.js} +4 -4
- package/dist/{cli-z8a4mfz1.js → cli-aqp59jmv.js} +4 -4
- package/dist/{cli-5d8wd50e.js → cli-bs0fwb23.js} +1 -1
- package/dist/{cli-fa4kxxx3.js → cli-ct5920jb.js} +2 -2
- package/dist/{cli-0awbtmv2.js → cli-dek8hqjc.js} +10 -10
- package/dist/{cli-qp6adcmw.js → cli-e24w2mvk.js} +1 -1
- package/dist/{cli-t3npd8hm.js → cli-ghfhn1qc.js} +1 -1
- package/dist/{cli-jmcf8cnz.js → cli-ja369zab.js} +1 -1
- package/dist/{cli-wtjg6je3.js → cli-jhh7tbx6.js} +1 -1
- package/dist/{cli-e19bgatr.js → cli-jmk31131.js} +2 -2
- package/dist/{cli-2cj8fqv1.js → cli-kjcdzea8.js} +1 -1
- package/dist/{cli-1xpr5992.js → cli-kjha1q9k.js} +3 -3
- package/dist/{cli-9jfty8px.js → cli-ktcdczth.js} +538 -30
- package/dist/{cli-exd52qn4.js → cli-mcmpbpcc.js} +2 -2
- package/dist/{cli-2m2p3snc.js → cli-mmf43vcp.js} +6 -6
- package/dist/{cli-b2rhbaty.js → cli-pnm99spe.js} +10 -10
- package/dist/{cli-9jn9cj0q.js → cli-v9jk2k63.js} +1 -1
- package/dist/{cli-mt7bxjfd.js → cli-vhgcwppn.js} +1 -1
- package/dist/{cli-qa4hgda3.js → cli-vvz64zc5.js} +14 -13
- package/dist/{cli-txr0z97c.js → cli-wmhajw68.js} +171 -32
- package/dist/{cli-hzrmv8pg.js → cli-x38rmhap.js} +3 -3
- package/dist/{cli-hq9hd4hp.js → cli-xz6qjdqj.js} +1 -1
- package/dist/{cli-g9ypd5en.js → cli-z52gq55b.js} +4 -0
- package/dist/{cli-2qbmjfta.js → cli-z6219sd3.js} +4 -4
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-zm1bkdde.js → codex-adapter-46kbvvkx.js} +1 -1
- package/dist/{codex-hook-tkag7fxq.js → codex-hook-tm9ecrd5.js} +1 -1
- package/dist/{codex-session-runner-8k02mqw2.js → codex-session-runner-qy93hdrv.js} +1 -1
- package/dist/{commands-nw1dn4av.js → commands-hwdhacfx.js} +2 -2
- package/dist/{controller-3kkkmps0.js → controller-hjza5p1f.js} +3 -3
- package/dist/{controllers-connect-n2c4167r.js → controllers-connect-7m25cay2.js} +20 -20
- package/dist/{cursor-adapter-h5028emw.js → cursor-adapter-1qh7ps8w.js} +1 -1
- package/dist/{db-76y2msnf.js → db-w23k9301.js} +2 -2
- package/dist/{db-8rk0p8j1.js → db-wm8by383.js} +3 -3
- package/dist/{dispatcher-xp4wsx30.js → dispatcher-en25ebgc.js} +18 -18
- package/dist/{dsh-adapter-vyrr31bn.js → dsh-adapter-zkme32vd.js} +1 -1
- package/dist/{e2b-eesfj689.js → e2b-9z9mvt2p.js} +1 -1
- package/dist/{e2b-03nha1wz.js → e2b-yhvn70es.js} +1 -1
- package/dist/{embedding-space-ejefyep1.js → embedding-space-rgf4aypt.js} +6 -6
- package/dist/{embeddings-w297xaw0.js → embeddings-z9cmxhx1.js} +1 -1
- package/dist/{external-channel-mf9vp1fe.js → external-channel-g6vv85zv.js} +2 -2
- package/dist/{grok-adapter-mpzyyprz.js → grok-adapter-f6n1tpq3.js} +2 -2
- package/dist/handlers-ss5948rq.js +42 -0
- package/dist/{heartbeat-qnaycjzs.js → heartbeat-5xcdzz45.js} +21 -21
- package/dist/{hook-h6sqfzxr.js → hook-6423rme3.js} +2 -2
- package/dist/{hook-4zqdgea4.js → hook-8dk1csdd.js} +4 -4
- package/dist/{http-65ts210h.js → http-6mfh2ww9.js} +255 -60
- package/dist/{index-sqe7f6mj.js → index-0js6bgff.js} +1 -1
- package/dist/{index-farm1zsn.js → index-0ng1dd1t.js} +10937 -9527
- package/dist/{index-k3ewf1jg.js → index-5eyn1wc5.js} +18 -18
- package/dist/{index-vp4wa3qn.js → index-71gnebsn.js} +18 -18
- package/dist/{index-hdfad00b.js → index-q5gzpjdd.js} +12 -10
- package/dist/index-z3b1vfjx.js +44 -0
- package/dist/{keepalive-52k6skha.js → keepalive-cymckeg6.js} +3 -3
- package/dist/{lead-y9rj0gr1.js → lead-baknj2dh.js} +29 -29
- package/dist/{maintenance-f86wqfq6.js → maintenance-dfrnsx3n.js} +1 -1
- package/dist/{oauth-refresh-sweep-vddv9ky7.js → oauth-refresh-sweep-5dztk8x6.js} +2 -2
- package/dist/{onboard-8z2qk6gq.js → onboard-h36vja6y.js} +11 -6
- package/dist/{otel-impl-epet9j18.js → otel-impl-jerq8vz8.js} +1 -1
- package/dist/{pricing-refresh-6wm42a51.js → pricing-refresh-t3nx5krn.js} +3 -3
- package/dist/{rbac-roles-2kfcs1sc.js → rbac-roles-dw1awawd.js} +2 -2
- package/dist/{rbac-roles-13a0vj6d.js → rbac-roles-km0fc3e8.js} +1 -1
- package/dist/{registry-ajhexe44.js → registry-hzfh268v.js} +1 -1
- package/dist/{render-v2-cy8zsfsf.js → render-v2-65x4ayj8.js} +3 -3
- package/dist/{resume-c783qe6b.js → resume-61znk3hj.js} +6 -6
- package/dist/{session-access-s4tv0az8.js → session-access-4yc9htrg.js} +4 -4
- package/dist/{session-source-backfill-qzapz0ne.js → session-source-backfill-kr5bdjtp.js} +2 -2
- package/dist/{setup-st7htx1k.js → setup-9h61yr2p.js} +1 -1
- package/dist/{work-objects-ftj4zw70.js → work-objects-x7rb53qn.js} +27 -22
- package/dist/{worker-859fk25k.js → worker-7f3khgn0.js} +29 -29
- package/openapi.json +134 -3
- package/package.json +5 -5
- package/src/be/db/agents.ts +16 -1
- package/src/be/db/runtime.ts +25 -15
- package/src/be/db/session-summaries.ts +120 -0
- package/src/be/db/tasks/read.ts +7 -4
- package/src/be/db/tasks/search-index.ts +159 -0
- package/src/be/db/tasks/write.ts +3 -0
- package/src/be/db-queries/slack-inbound.ts +77 -0
- package/src/be/db.ts +27 -8
- package/src/be/mcp-overflow-sweep.ts +39 -0
- package/src/be/migrations/253_session_summaries.sql +300 -0
- package/src/be/migrations/254_query_audit_indexes.sql +25 -0
- package/src/be/migrations/256_agent_slack_icon.sql +2 -0
- package/src/be/migrations/257_agent_tasks_fts.sql +55 -0
- package/src/comb/config.ts +6 -2
- package/src/comb/requester.ts +69 -0
- package/src/comb/review-batch.ts +24 -2
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/onboard/compose-generator.ts +7 -2
- package/src/commands/onboard/env-generator.ts +8 -3
- package/src/commands/runner.ts +41 -24
- package/src/fs/agent-fs-provider.ts +9 -0
- package/src/heartbeat/heartbeat.ts +13 -6
- package/src/http/agents.ts +10 -2
- package/src/http/index.ts +15 -0
- package/src/http/integrations.ts +20 -0
- package/src/http/list-total-cache.ts +62 -0
- package/src/http/sessions.ts +9 -2
- package/src/http/slack-inbound.ts +1 -0
- package/src/http/status.ts +4 -1
- package/src/http/tasks.ts +12 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/queue-stall-alarm.ts +4 -1
- package/src/slack/actions.ts +2 -2
- package/src/slack/app-icon.ts +50 -0
- package/src/slack/app.ts +13 -1
- package/src/slack/assistant.ts +22 -1
- package/src/slack/event-dedup.test.ts +11 -0
- package/src/slack/event-dedup.ts +8 -4
- package/src/slack/inbound-dispatch.ts +58 -0
- package/src/slack/render-v2.ts +7 -8
- package/src/slack/responses.ts +21 -8
- package/src/slack/socket-ack.ts +237 -0
- package/src/slack/thread-buffer.ts +3 -5
- package/src/slack/watcher.ts +2 -2
- package/src/slack/work-objects.ts +5 -1
- package/src/tests/agent-fs-release-notes.test.ts +40 -0
- package/src/tests/agent-slack-icon.test.ts +85 -0
- package/src/tests/approval-cancellation-migration.test.ts +2 -0
- package/src/tests/asset-key-migration.test.ts +26 -0
- package/src/tests/comb-default.test.ts +159 -0
- package/src/tests/comb-review-batch.test.ts +192 -4
- package/src/tests/comb-status.test.ts +65 -2
- package/src/tests/db-agents.test.ts +13 -0
- package/src/tests/db-tasks-read.test.ts +10 -15
- package/src/tests/deploy-one-box.test.ts +161 -0
- package/src/tests/deploy-stubs.ts +68 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/e2e-codex-auth-chain.test.ts +18 -1
- package/src/tests/fixtures/deploy-stubs/bun +3 -0
- package/src/tests/fixtures/deploy-stubs/bunx +27 -0
- package/src/tests/fixtures/deploy-stubs/curl +26 -0
- package/src/tests/fixtures/deploy-stubs/docker +122 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +5 -5
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +4 -4
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +12 -12
- package/src/tests/fixtures/onboard/full.compose.yml.txt +12 -12
- package/src/tests/fixtures/onboard/research.compose.yml.txt +4 -4
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +2 -2
- package/src/tests/heartbeat-expiry.test.ts +151 -25
- package/src/tests/http-api-integration.test.ts +31 -0
- package/src/tests/human-free-reclassify.test.ts +6 -0
- package/src/tests/install-sh-bind.test.ts +242 -0
- package/src/tests/install-sh-tunnel.test.ts +89 -0
- package/src/tests/kv-storage.test.ts +28 -0
- package/src/tests/list-endpoint-slimming.test.ts +3 -0
- package/src/tests/list-total-cache.test.ts +63 -0
- package/src/tests/mcp-overflow-sweep.test.ts +84 -0
- package/src/tests/onboard-compose.test.ts +17 -3
- package/src/tests/onboard-env.test.ts +13 -0
- package/src/tests/onboarding.test.ts +1 -1
- package/src/tests/preload.ts +1 -0
- package/src/tests/query-audit-plans.test.ts +314 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/session-summaries.test.ts +384 -0
- package/src/tests/sessions-tree.test.ts +5 -1
- package/src/tests/slack-app-icon.test.ts +90 -0
- package/src/tests/slack-external-channel-egress.test.ts +1 -1
- package/src/tests/slack-inbound-receipts.test.ts +34 -0
- package/src/tests/slack-inline-output.test.ts +29 -2
- package/src/tests/slack-render-v2-query-plans.test.ts +3 -2
- package/src/tests/slack-render-v2.test.ts +35 -0
- package/src/tests/slack-socket-ack.test.ts +553 -0
- package/src/tests/task-lineage.test.ts +3 -0
- package/src/tests/task-search-filter.test.ts +4 -4
- package/src/tests/task-search-fts.test.ts +320 -0
- package/src/tests/update-profile-auth.test.ts +28 -0
- package/src/tools/get-swarm.ts +1 -0
- package/src/tools/get-tasks.ts +6 -1
- package/src/tools/join-swarm.ts +1 -0
- package/src/tools/my-agent-info.ts +1 -0
- package/src/tools/slack-post.ts +2 -2
- package/src/tools/slack-reply.ts +2 -2
- package/src/tools/slack-start-thread.ts +2 -2
- package/src/tools/slack-update.ts +1 -1
- package/src/tools/update-profile.ts +20 -4
- package/src/tools/utils.ts +3 -4
- package/src/types.ts +47 -0
- package/src/utils/constants.ts +127 -5
- package/templates/official/lead/HEARTBEAT.md +4 -2
- package/templates/official/lead/config.json +2 -2
- package/templates/skills/swarm-docs/content.md +2 -0
- package/dist/handlers-7x8sywgr.js +0 -42
- package/dist/index-mc30r1hs.js +0 -44
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
countScriptRuns,
|
|
7
|
+
countSessions,
|
|
8
|
+
createAgent,
|
|
9
|
+
createTaskExtended,
|
|
10
|
+
getAllTasks,
|
|
11
|
+
getDb,
|
|
12
|
+
getDbClient,
|
|
13
|
+
getRecentCompletedCount,
|
|
14
|
+
getStuckWorkflowRuns,
|
|
15
|
+
getTaskByWorkflowRunStepId,
|
|
16
|
+
getTasksCount,
|
|
17
|
+
initDb,
|
|
18
|
+
updateAgentProfile,
|
|
19
|
+
} from "../be/db";
|
|
20
|
+
import { checkHeartbeatChecklist } from "../heartbeat/heartbeat";
|
|
21
|
+
import { getQueueStallSnapshot } from "../queue-stall-alarm";
|
|
22
|
+
import { AgentTaskStatusSchema, OPEN_TASK_STATUSES, TERMINAL_TASK_STATUSES } from "../types";
|
|
23
|
+
|
|
24
|
+
const TEST_DB_PATH = "./test-query-audit-plans.sqlite";
|
|
25
|
+
|
|
26
|
+
async function removeDbFiles(): Promise<void> {
|
|
27
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
28
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Captured {
|
|
32
|
+
sql: string;
|
|
33
|
+
params: unknown[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Runs the real caller and returns the one statement it issued that matches
|
|
37
|
+
// `match`, so the plans below are of the SQL production sends, not a copy.
|
|
38
|
+
async function capturedSql(match: RegExp, run: () => Promise<unknown>): Promise<Captured> {
|
|
39
|
+
const client = getDbClient();
|
|
40
|
+
const seen: Captured[] = [];
|
|
41
|
+
const query = client.query.bind(client);
|
|
42
|
+
const get = client.get.bind(client);
|
|
43
|
+
const record = (sql: string, params?: unknown[]) => {
|
|
44
|
+
if (match.test(sql)) seen.push({ sql, params: params ?? [] });
|
|
45
|
+
};
|
|
46
|
+
const spies = [
|
|
47
|
+
spyOn(client, "query").mockImplementation(((sql: string, params?: unknown[]) => {
|
|
48
|
+
record(sql, params);
|
|
49
|
+
return query(sql, params);
|
|
50
|
+
}) as typeof client.query),
|
|
51
|
+
spyOn(client, "get").mockImplementation(((sql: string, params?: unknown[]) => {
|
|
52
|
+
record(sql, params);
|
|
53
|
+
return get(sql, params);
|
|
54
|
+
}) as typeof client.get),
|
|
55
|
+
];
|
|
56
|
+
try {
|
|
57
|
+
await run();
|
|
58
|
+
} finally {
|
|
59
|
+
for (const spy of spies) spy.mockRestore();
|
|
60
|
+
}
|
|
61
|
+
expect(seen).toHaveLength(1);
|
|
62
|
+
return seen[0]!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function plan({ sql, params }: Captured, db: Database = getDb()): string {
|
|
66
|
+
return db
|
|
67
|
+
.query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
|
|
68
|
+
.all(...(params as any[]))
|
|
69
|
+
.map((row) => row.detail)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Plan of `captured` on a copy of the DB with `index` dropped, i.e. with
|
|
74
|
+
// migration 254 reverted.
|
|
75
|
+
function planWithoutIndex(captured: Captured, index: string): string {
|
|
76
|
+
const copy = Database.deserialize(getDb().serialize());
|
|
77
|
+
try {
|
|
78
|
+
copy.run(`DROP INDEX ${index}`);
|
|
79
|
+
return plan(captured, copy);
|
|
80
|
+
} finally {
|
|
81
|
+
copy.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Production has no sqlite_stat1, so the planner picks indexes without
|
|
86
|
+
// statistics. Each case runs the real caller, EXPLAINs the statement it
|
|
87
|
+
// issued, and checks the plan reaches its index. The regression control
|
|
88
|
+
// reverts that case's fix (the SQL shape or the migration 254 index) and
|
|
89
|
+
// checks the same assertion then fails, so a green case is not vacuous.
|
|
90
|
+
const CASES: Array<{
|
|
91
|
+
name: string;
|
|
92
|
+
match: RegExp;
|
|
93
|
+
run: () => Promise<unknown>;
|
|
94
|
+
uses: string[];
|
|
95
|
+
revert: { sql: (sql: string) => string } | { dropIndex: string };
|
|
96
|
+
}> = [
|
|
97
|
+
{
|
|
98
|
+
name: "getTaskByWorkflowRunStepId",
|
|
99
|
+
match: /workflowRunStepId/,
|
|
100
|
+
run: () => getTaskByWorkflowRunStepId("step-1"),
|
|
101
|
+
uses: [
|
|
102
|
+
"SEARCH agent_tasks USING INDEX idx_agent_tasks_workflowRunStepId (workflowRunStepId=?)",
|
|
103
|
+
],
|
|
104
|
+
revert: { dropIndex: "idx_agent_tasks_workflowRunStepId" },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "getStuckWorkflowRuns",
|
|
108
|
+
match: /workflow_run_steps/,
|
|
109
|
+
run: () => getStuckWorkflowRuns(),
|
|
110
|
+
uses: [
|
|
111
|
+
"SEARCH wrs USING INDEX idx_wrs_status (status=?)",
|
|
112
|
+
"SEARCH at USING INDEX idx_agent_tasks_workflowRunStepId (workflowRunStepId=?)",
|
|
113
|
+
],
|
|
114
|
+
revert: { dropIndex: "idx_agent_tasks_workflowRunStepId" },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "captureRoutingDecisionSnapshot",
|
|
118
|
+
match: /FROM agents agent/,
|
|
119
|
+
run: () => createTaskExtended("routing snapshot", {}),
|
|
120
|
+
uses: ["MATERIALIZE open_task", "SEARCH agent_tasks USING INDEX idx_agent_tasks_status"],
|
|
121
|
+
revert: { sql: (sql) => sql.replace("AS MATERIALIZED", "AS NOT MATERIALIZED") },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "checkHeartbeatChecklist dedup",
|
|
125
|
+
match: /heartbeat-checklist/,
|
|
126
|
+
run: () => checkHeartbeatChecklist(),
|
|
127
|
+
uses: ["SEARCH agent_tasks USING INDEX idx_agent_tasks_status"],
|
|
128
|
+
revert: { sql: (sql) => sql.replace(/\+(agentId|taskType)/g, "$1") },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "getRecentCompletedCount",
|
|
132
|
+
match: /agent_tasks/,
|
|
133
|
+
run: () => getRecentCompletedCount(),
|
|
134
|
+
uses: ["COVERING INDEX idx_agent_tasks_status_finishedAt (status=? AND finishedAt>?)"],
|
|
135
|
+
revert: { dropIndex: "idx_agent_tasks_status_finishedAt" },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "countScriptRuns by scriptName",
|
|
139
|
+
match: /script_runs/,
|
|
140
|
+
run: () => countScriptRuns({ scriptName: "daily" }),
|
|
141
|
+
uses: ["COVERING INDEX idx_script_runs_scriptName_startedAt (scriptName=?)"],
|
|
142
|
+
revert: { dropIndex: "idx_script_runs_scriptName_startedAt" },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "getQueueStallSnapshot pickups",
|
|
146
|
+
match: /COUNT\(\*\) AS count\s+FROM agent_log/,
|
|
147
|
+
run: () => getQueueStallSnapshot(),
|
|
148
|
+
uses: ["SEARCH agent_log USING INDEX idx_agent_log_createdAt (createdAt>?)"],
|
|
149
|
+
revert: { sql: (sql) => sql.replace("+eventType", "eventType") },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "countSessions by source",
|
|
153
|
+
match: /COUNT/,
|
|
154
|
+
run: () => countSessions({ source: ["slack"] }),
|
|
155
|
+
uses: ["COVERING INDEX idx_agent_tasks_parentTaskId_source (parentTaskId=? AND source=?)"],
|
|
156
|
+
revert: { dropIndex: "idx_agent_tasks_parentTaskId_source" },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
// Few hits: the page reads the agent_tasks_fts candidates by rowid. The
|
|
160
|
+
// control is the old `task LIKE '%q%'`, which scans every task body.
|
|
161
|
+
name: "getAllTasks search",
|
|
162
|
+
match: /WITH page AS/,
|
|
163
|
+
run: async () => {
|
|
164
|
+
await createTaskExtended("update the rulebook", {});
|
|
165
|
+
return getAllTasks({ search: "rulebook" });
|
|
166
|
+
},
|
|
167
|
+
uses: ["CO-ROUTINE page\nSEARCH agent_tasks USING INTEGER PRIMARY KEY (rowid=?)"],
|
|
168
|
+
revert: {
|
|
169
|
+
sql: (sql) =>
|
|
170
|
+
sql.replace(
|
|
171
|
+
/agent_tasks\.rowid IN \([\d, ]+\)/,
|
|
172
|
+
"(task LIKE '%rulebook%' OR title LIKE '%rulebook%' OR id LIKE '%rulebook%')",
|
|
173
|
+
),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
function missing(detail: string, uses: string[]): string[] {
|
|
179
|
+
return uses.filter((step) => !detail.includes(step));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
describe("query audit plans without sqlite_stat1", () => {
|
|
183
|
+
beforeAll(async () => {
|
|
184
|
+
await removeDbFiles();
|
|
185
|
+
initDb(TEST_DB_PATH);
|
|
186
|
+
// checkHeartbeatChecklist reaches its dedup only for a lead with a
|
|
187
|
+
// checklist, and stops there when an open checklist task exists.
|
|
188
|
+
const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
189
|
+
await updateAgentProfile(lead.id, { heartbeatMd: "# Checklist\n\n- Check stuck tasks\n" });
|
|
190
|
+
await createTaskExtended("open checklist", {
|
|
191
|
+
agentId: lead.id,
|
|
192
|
+
taskType: "heartbeat-checklist",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterAll(async () => {
|
|
197
|
+
closeDb();
|
|
198
|
+
await removeDbFiles();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("the test DB has no planner statistics, like production", async () => {
|
|
202
|
+
const stat = await getDbClient().get<{ name: string }>(
|
|
203
|
+
"SELECT name FROM sqlite_master WHERE name = 'sqlite_stat1'",
|
|
204
|
+
);
|
|
205
|
+
expect(stat).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
for (const { name, match, run, uses, revert } of CASES) {
|
|
209
|
+
test(`${name} uses its index, and loses it when the fix is reverted`, async () => {
|
|
210
|
+
const captured = await capturedSql(match, run);
|
|
211
|
+
const detail = plan(captured);
|
|
212
|
+
expect(missing(detail, uses)).toEqual([]);
|
|
213
|
+
expect(detail).not.toContain("SCAN agent_tasks");
|
|
214
|
+
|
|
215
|
+
const reverted =
|
|
216
|
+
"dropIndex" in revert
|
|
217
|
+
? planWithoutIndex(captured, revert.dropIndex)
|
|
218
|
+
: plan({ ...captured, sql: revert.sql(captured.sql) });
|
|
219
|
+
expect(missing(reverted, uses)).not.toEqual([]);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test("task search with many hits pages by lastUpdatedAt and counts from the index", async () => {
|
|
224
|
+
await getDbClient().transaction(async (tx) => {
|
|
225
|
+
for (let i = 0; i < 501; i++) {
|
|
226
|
+
await tx.run(
|
|
227
|
+
`INSERT INTO agent_tasks (id, task, status, source, createdAt, lastUpdatedAt)
|
|
228
|
+
VALUES (?, ?, 'completed', 'api', ?, ?)`,
|
|
229
|
+
[`landing-${i}`, `landing page ${i}`, "2026-10-01T00:00:00.000Z", `2026-10-01T${i}`],
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// The page walks its ORDER BY index and stops at LIMIT, testing each row
|
|
235
|
+
// against the FTS hits. Without the `+`, the planner drives from the
|
|
236
|
+
// hits and sorts them all, reading every matching task body.
|
|
237
|
+
const page = await capturedSql(/WITH page AS/, () => getAllTasks({ search: "landing" }));
|
|
238
|
+
const pageUses = [
|
|
239
|
+
"CO-ROUTINE page\nSCAN agent_tasks USING INDEX idx_agent_tasks_last_updated",
|
|
240
|
+
"SCAN agent_tasks_fts VIRTUAL TABLE",
|
|
241
|
+
];
|
|
242
|
+
expect(page.sql).not.toContain("task LIKE");
|
|
243
|
+
expect(missing(plan(page), pageUses)).toEqual([]);
|
|
244
|
+
const pageReverted = plan({
|
|
245
|
+
...page,
|
|
246
|
+
sql: page.sql.replace("+agent_tasks.rowid", "agent_tasks.rowid"),
|
|
247
|
+
});
|
|
248
|
+
expect(missing(pageReverted, pageUses)).not.toEqual([]);
|
|
249
|
+
|
|
250
|
+
// A count has no LIMIT to stop at, so it drives from the hits.
|
|
251
|
+
const count = await capturedSql(/COUNT/, () => getTasksCount({ search: "landing" }));
|
|
252
|
+
const countUses = [
|
|
253
|
+
"SCAN agent_tasks_fts VIRTUAL TABLE",
|
|
254
|
+
"SEARCH agent_tasks USING INTEGER PRIMARY KEY (rowid=?)",
|
|
255
|
+
];
|
|
256
|
+
expect(count.sql).not.toContain("task LIKE");
|
|
257
|
+
expect(missing(plan(count), countUses)).toEqual([]);
|
|
258
|
+
expect(plan(count)).not.toContain("SCAN agent_tasks USING");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("getStuckWorkflowRuns returns only finished tasks of waiting steps in waiting runs", async () => {
|
|
262
|
+
const client = getDbClient();
|
|
263
|
+
await client.run("PRAGMA foreign_keys = OFF");
|
|
264
|
+
const now = "2026-10-27T00:00:00.000Z";
|
|
265
|
+
const cases = [
|
|
266
|
+
{ id: "stuck", runStatus: "waiting", stepStatus: "waiting", taskStatus: "completed" },
|
|
267
|
+
{
|
|
268
|
+
id: "running-task",
|
|
269
|
+
runStatus: "waiting",
|
|
270
|
+
stepStatus: "waiting",
|
|
271
|
+
taskStatus: "in_progress",
|
|
272
|
+
},
|
|
273
|
+
{ id: "done-step", runStatus: "waiting", stepStatus: "completed", taskStatus: "failed" },
|
|
274
|
+
{ id: "running-run", runStatus: "running", stepStatus: "waiting", taskStatus: "cancelled" },
|
|
275
|
+
];
|
|
276
|
+
for (const c of cases) {
|
|
277
|
+
await client.run(
|
|
278
|
+
"INSERT INTO workflow_runs (id, workflowId, status, startedAt) VALUES (?, 'wf', ?, ?)",
|
|
279
|
+
[`run-${c.id}`, c.runStatus, now],
|
|
280
|
+
);
|
|
281
|
+
await client.run(
|
|
282
|
+
`INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, status, startedAt)
|
|
283
|
+
VALUES (?, ?, 'node', 'agent-task', ?, ?)`,
|
|
284
|
+
[`step-${c.id}`, `run-${c.id}`, c.stepStatus, now],
|
|
285
|
+
);
|
|
286
|
+
await client.run(
|
|
287
|
+
`INSERT INTO agent_tasks (id, task, status, output, workflowRunStepId, createdAt, lastUpdatedAt)
|
|
288
|
+
VALUES (?, 'task', ?, 'out', ?, ?, ?)`,
|
|
289
|
+
[`task-${c.id}`, c.taskStatus, `step-${c.id}`, now, now],
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
expect(await getStuckWorkflowRuns()).toEqual([
|
|
294
|
+
{
|
|
295
|
+
runId: "run-stuck",
|
|
296
|
+
stepId: "step-stuck",
|
|
297
|
+
nodeId: "node",
|
|
298
|
+
taskId: "task-stuck",
|
|
299
|
+
taskStatus: "completed",
|
|
300
|
+
taskOutput: "out",
|
|
301
|
+
workflowId: "wf",
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("OPEN_TASK_STATUSES", () => {
|
|
308
|
+
test("open and terminal statuses partition every task status", () => {
|
|
309
|
+
const all = [...AgentTaskStatusSchema.options].sort();
|
|
310
|
+
expect([...OPEN_TASK_STATUSES, ...TERMINAL_TASK_STATUSES].sort()).toEqual(all);
|
|
311
|
+
const terminal = new Set<string>(TERMINAL_TASK_STATUSES);
|
|
312
|
+
expect(OPEN_TASK_STATUSES.filter((status) => terminal.has(status))).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -659,6 +659,12 @@ describe("script connections", () => {
|
|
|
659
659
|
markMigrationApplied(database, "247_workflow_run_triggered_by_agent.sql");
|
|
660
660
|
// 250 indexes workflow_run_steps, which this migration-112-only fixture does not create.
|
|
661
661
|
markMigrationApplied(database, "250_workflow_run_steps_runid_status_index.sql");
|
|
662
|
+
// 253 adds triggers on agent_tasks columns and approval_requests, which this fixture lacks.
|
|
663
|
+
markMigrationApplied(database, "253_session_summaries.sql");
|
|
664
|
+
// 254 indexes agent_tasks columns that this fixture's id-only agent_tasks lacks.
|
|
665
|
+
markMigrationApplied(database, "254_query_audit_indexes.sql");
|
|
666
|
+
// 256 alters agents, which this migration-112-only fixture does not create.
|
|
667
|
+
markMigrationApplied(database, "256_agent_slack_icon.sql");
|
|
662
668
|
|
|
663
669
|
const id = crypto.randomUUID();
|
|
664
670
|
const now = new Date().toISOString();
|