@desplega.ai/agent-swarm 2.7.5 → 2.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-g3ads55s.js → acp-adapter-bp91d37f.js} +4 -4
- package/dist/{actions-zwfykqqk.js → actions-yj0eeaqh.js} +18 -17
- package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-4pg9h34q.js} +1 -1
- package/dist/{app-vmp4fgep.js → app-909e97cr.js} +3 -3
- package/dist/{assistant-vwde653f.js → assistant-qdyb5fyy.js} +24 -22
- package/dist/{boot-reembed-375t7tbk.js → boot-reembed-fk0s9z18.js} +1 -1
- package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-gs1rzrnh.js} +1 -1
- package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-4qfd4kfx.js} +2 -2
- package/dist/{boot-seeds-aykx51v3.js → boot-seeds-yr00k13d.js} +4 -4
- package/dist/{boxd-k1j26g3h.js → boxd-vdmfssd9.js} +4 -4
- package/dist/{claude-adapter-8j4hm099.js → claude-adapter-qta1f1xn.js} +3 -3
- package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-sc6j4dm0.js} +1 -1
- package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-zgrz6yvc.js} +9169 -9070
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-vqypxzh4.js → cli-0at8w34w.js} +7 -7
- package/dist/{cli-7a42nfy0.js → cli-0qatpy8q.js} +3 -3
- package/dist/{cli-p599f1qm.js → cli-0yv5z2pg.js} +1 -1
- package/dist/{cli-0gey5fzy.js → cli-386tyxxg.js} +2 -2
- package/dist/{cli-5pag6rmn.js → cli-3ypk9vk1.js} +1 -1
- package/dist/{cli-qxnf8z2z.js → cli-436532ck.js} +2 -2
- package/dist/{cli-z8eh3ex8.js → cli-4necwtsv.js} +4 -4
- package/dist/{cli-zh3fp8v2.js → cli-54t00gt8.js} +1 -1
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-nn4d8dne.js → cli-89knwags.js} +2 -2
- package/dist/{cli-spknvxbq.js → cli-8bc1w0tw.js} +10 -10
- package/dist/{cli-rqkxrmgw.js → cli-8snvgs5r.js} +38 -13
- package/dist/{cli-rhw40jgb.js → cli-bjb3z51z.js} +2 -2
- package/dist/{cli-xtjhfmpc.js → cli-c60vetmm.js} +1 -1
- package/dist/{cli-0731hvbw.js → cli-decfb6v8.js} +2 -2
- package/dist/{cli-bhjt02p8.js → cli-drr5v6av.js} +1 -1
- package/dist/{cli-fqzx7gwa.js → cli-dxae2yg7.js} +2 -2
- package/dist/{cli-tqgg40ek.js → cli-e3r2neck.js} +8 -7
- package/dist/{cli-zppwg9p9.js → cli-ekwks2vs.js} +1 -1
- package/dist/{cli-nzxr556s.js → cli-es6va8jk.js} +26 -26
- package/dist/{cli-a47xy8eq.js → cli-faxe6thg.js} +1 -1
- package/dist/cli-gdhpcbhf.js +55 -0
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
- package/dist/{cli-pywf3eh5.js → cli-hgaazk69.js} +1 -1
- package/dist/{cli-0bxqnvq9.js → cli-hr2e2fft.js} +28 -6
- package/dist/{cli-1ayk1eas.js → cli-k2gzwsj2.js} +135 -26
- package/dist/{cli-xbkdm710.js → cli-kqdg1bsb.js} +3 -3
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-g61qxmge.js → cli-m48g6p95.js} +3 -3
- package/dist/{cli-7wspm5gm.js → cli-nhrvpaj7.js} +20 -4
- package/dist/{cli-afrrppsw.js → cli-nwj0ee2w.js} +2 -2
- package/dist/{cli-3h4nhjn8.js → cli-p5gp8pcx.js} +9 -7
- package/dist/{cli-rm6hqj3z.js → cli-pd7vagab.js} +33 -139
- package/dist/{cli-mjzynrse.js → cli-q0k1w5zd.js} +1 -1
- package/dist/{cli-8hs41zy2.js → cli-rmmmrjkd.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1338796d.js → cli-shpbx5ee.js} +142 -98
- package/dist/{cli-82ae98pz.js → cli-svj5090z.js} +2 -2
- package/dist/{cli-bnz7a7qf.js → cli-vvpx3gsx.js} +1 -1
- package/dist/{cli-mmwm7zbf.js → cli-w6dt9ged.js} +1 -1
- package/dist/{cli-wvaawacd.js → cli-waqbxg08.js} +5230 -66
- package/dist/{cli-j3jvnpe8.js → cli-wp9sp1qb.js} +1 -1
- package/dist/{cli-nmbmy64z.js → cli-xbbndvjy.js} +3 -3
- package/dist/{cli-vbyfwh92.js → cli-xds5f77d.js} +2 -2
- package/dist/{cli-7wcyk2s5.js → cli-xnqgpshy.js} +1 -1
- package/dist/{cli-rpm2sgv0.js → cli-xnrwazav.js} +19 -2
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-mry0qzym.js → cli-yn4kxs06.js} +123 -10
- package/dist/{cli-hn9hxmz9.js → cli-yx4kfngt.js} +1 -1
- package/dist/{cli-y24sc29k.js → cli-yy3rhsdw.js} +30 -8
- package/dist/{cli-6s7vdcta.js → cli-zbbwez0r.js} +4 -4
- package/dist/{cli-z6jkx23q.js → cli-zm72anhg.js} +9 -9
- package/dist/cli.js +22 -21
- package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-ayd1fb6h.js} +2 -2
- package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-jesa32xc.js} +2 -2
- package/dist/{commands-evrenjn9.js → commands-hanewgf2.js} +9 -3
- package/dist/{controller-xjp8vswh.js → controller-dt1n3c1q.js} +8 -3
- package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-xx8x64n5.js} +20 -19
- package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-9s4dwwdw.js} +1 -1
- package/dist/{db-zxpw6aht.js → db-3vttcbv4.js} +3 -3
- package/dist/{db-rgfsh91f.js → db-9dv0g1ab.js} +11 -3
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-smasgws7.js → dispatcher-tx9hd1sv.js} +17 -16
- package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
- package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-007y4ejs.js} +1 -1
- package/dist/{e2b-qcaj675y.js → e2b-a19d28rz.js} +2 -2
- package/dist/{e2b-w1p0bnpm.js → e2b-we6hs6q5.js} +4 -4
- package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-rgbrqb8s.js} +1 -1
- package/dist/{embedding-space-1z0mbg95.js → embedding-space-ny1e2940.js} +6 -6
- package/dist/{embeddings-ygeg7tm7.js → embeddings-2t386t13.js} +1 -1
- package/dist/{execute-14qhqp4d.js → execute-pksh0ceh.js} +1 -1
- package/dist/{external-channel-ttgrs3sx.js → external-channel-phgmhmd7.js} +2 -2
- package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-fy8rr921.js} +5 -5
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
- package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-q0abvpaj.js} +5 -5
- package/dist/handlers-by3sfxyx.js +32 -0
- package/dist/{heartbeat-qahz4p62.js → heartbeat-kadyek8p.js} +20 -19
- package/dist/{hook-kj3d398m.js → hook-5zs3msf6.js} +2 -2
- package/dist/{hook-4fd5wq5w.js → hook-9k551xnj.js} +2 -2
- package/dist/{http-1ted0j1k.js → http-mhz9vw1f.js} +213 -80
- package/dist/index-4f2pvxae.js +37 -0
- package/dist/index-98gz7tmp.js +30 -0
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k08z2cak.js +33 -0
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/{index-6rq3r5zb.js → index-q361cg2f.js} +1 -1
- package/dist/{index-7v51awbn.js → index-rvr00sg7.js} +20 -13
- package/dist/{index-q4b917zs.js → index-vdygbq10.js} +1 -1
- package/dist/{keepalive-6cf311t5.js → keepalive-w56f32cg.js} +3 -3
- package/dist/{lead-fjy418cw.js → lead-enq35nqp.js} +31 -30
- package/dist/{maintenance-a4bm1bmt.js → maintenance-7mdgxcea.js} +1 -1
- package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-29txy9vk.js} +2 -2
- package/dist/{onboard-myn5hraa.js → onboard-kpsrkarj.js} +4 -4
- package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-j25vm3rr.js} +3 -3
- package/dist/{otel-impl-a1a0ef6b.js → otel-impl-6kkct7a8.js} +1 -1
- package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-exhkzhn2.js} +4 -4
- package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-n2x7jh6k.js} +3 -3
- package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-81ypx6es.js} +1 -1
- package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-b5htkjby.js} +2 -2
- package/dist/{render-v2-twmbgrz4.js → render-v2-qn6jmpk4.js} +3 -3
- package/dist/{resume-zfqc36xj.js → resume-wd30wx23.js} +6 -6
- package/dist/{runtime-qwggdpqr.js → runtime-4hsg7h60.js} +1 -1
- package/dist/{session-access-sc9pn9da.js → session-access-5qvfevhk.js} +4 -4
- package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-n1czavbr.js} +2 -2
- package/dist/{setup-4fr0m7a1.js → setup-dnb56qy5.js} +3 -3
- package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-76xxv9fs.js} +2 -2
- package/dist/{work-objects-0wr8zxct.js → work-objects-4n21tfkm.js} +20 -19
- package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-77hvzxww.js} +17 -16
- package/dist/{worker-0c5tyzqe.js → worker-gg1576j3.js} +31 -30
- package/openapi.json +93 -1
- package/package.json +8 -7
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/tasks/read.ts +122 -3
- package/src/be/db-client.ts +8 -5
- package/src/be/db.ts +15 -4
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +22 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/http/index.ts +1 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/memory.ts +38 -9
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/fx-adapter.ts +1 -1
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/types/stdlib.d.ts +22 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
- package/src/server-user.ts +1 -0
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +37 -2
- package/src/slack/left-threads.ts +27 -4
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +8 -2
- package/src/tests/db-task-write-contract.test.ts +5 -1
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +14 -8
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/query-audit-plans.test.ts +27 -0
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/script-connections.test.ts +11 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-scripts.test.ts +26 -0
- package/src/tests/slack-leave-thread.test.ts +145 -5
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +1 -1
- package/src/tools/task-dedup.ts +45 -21
- package/src/types.ts +38 -0
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-vwmba1m2.js +0 -31
- package/dist/index-5r71rf98.js +0 -32
- package/dist/index-anmwxs87.js +0 -29
- package/dist/index-spp0ra5c.js +0 -36
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { createListTotalCache } from "../http/list-total-cache";
|
|
2
|
+
import { createListTotalCache, LIST_TOTAL_TTL_MS } from "../http/list-total-cache";
|
|
3
3
|
|
|
4
4
|
function counter(values: number[]) {
|
|
5
5
|
let calls = 0;
|
|
@@ -10,7 +10,7 @@ function counter(values: number[]) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
describe("list total cache", () => {
|
|
13
|
-
test("page 1 always counts; later pages reuse its count", async () => {
|
|
13
|
+
test("page 1 always counts by default; later pages reuse its count", async () => {
|
|
14
14
|
const cache = createListTotalCache();
|
|
15
15
|
const count = counter([10, 11, 12]);
|
|
16
16
|
|
|
@@ -24,6 +24,31 @@ describe("list total cache", () => {
|
|
|
24
24
|
expect(count.calls()).toBe(2);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
test("page 1 polls within the first-page TTL share one count", async () => {
|
|
28
|
+
const cache = createListTotalCache(LIST_TOTAL_TTL_MS, 50);
|
|
29
|
+
const count = counter([10, 11]);
|
|
30
|
+
expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(10);
|
|
31
|
+
expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(10);
|
|
32
|
+
expect(count.calls()).toBe(1);
|
|
33
|
+
await Bun.sleep(60);
|
|
34
|
+
expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(11);
|
|
35
|
+
expect(count.calls()).toBe(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("with a first-page TTL, concurrent page 1 requests share an in-flight count", async () => {
|
|
39
|
+
const cache = createListTotalCache(LIST_TOTAL_TTL_MS, 1000);
|
|
40
|
+
let calls = 0;
|
|
41
|
+
const compute = async () => {
|
|
42
|
+
calls++;
|
|
43
|
+
await Bun.sleep(5);
|
|
44
|
+
return 4;
|
|
45
|
+
};
|
|
46
|
+
expect(await Promise.all([cache.get({}, 0, compute), cache.get({}, 0, compute)])).toEqual([
|
|
47
|
+
4, 4,
|
|
48
|
+
]);
|
|
49
|
+
expect(calls).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
27
52
|
test("a cold later page counts, so total is always set", async () => {
|
|
28
53
|
const cache = createListTotalCache();
|
|
29
54
|
const count = counter([7]);
|
|
@@ -2,8 +2,8 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, createAgent, getDb, getDbClient, initDb } from "../be/db";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
FTS_SCAN_BATCH_SQL,
|
|
6
|
+
MEMORY_ID_BATCH_SQL,
|
|
7
7
|
SqliteMemoryStore,
|
|
8
8
|
} from "../be/memory/providers/sqlite-store";
|
|
9
9
|
|
|
@@ -62,16 +62,18 @@ describe("memory_fts boot backfill", () => {
|
|
|
62
62
|
// Positive control: the old anti-join scans memory_fts once per memory.
|
|
63
63
|
expect(planOf(QUADRATIC_BACKFILL_SQL).join(" | ")).toContain("CORRELATED");
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// Each batch seeks a rowid range of one table. The orphan check is a
|
|
66
|
+
// lookup on the agent_memory primary key, never a scan of memory_fts.
|
|
67
|
+
const scan = planOf(FTS_SCAN_BATCH_SQL.replace(/\?/g, "0")).join(" | ");
|
|
68
|
+
expect(scan).toMatch(/SCAN memory_fts VIRTUAL TABLE INDEX \d+:>/);
|
|
69
|
+
expect(scan.match(/memory_fts/g)?.length).toBe(1);
|
|
70
|
+
expect(scan).toContain("SEARCH m USING COVERING INDEX sqlite_autoindex_agent_memory_1 (id=?)");
|
|
71
|
+
const ids = planOf(MEMORY_ID_BATCH_SQL.replace(/\?/g, "0")).join(" | ");
|
|
72
|
+
expect(ids).toContain("SEARCH agent_memory USING INTEGER PRIMARY KEY (rowid>?)");
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
test("a new store backfills missing rows across batches and drops orphans", async () => {
|
|
74
|
-
const total = 1050; // crosses two 500-row batch
|
|
76
|
+
const total = 1050; // crosses two 500-row insert batches and one 1000-row scan batch
|
|
75
77
|
const seed = await store.store({
|
|
76
78
|
agentId,
|
|
77
79
|
scope: "agent",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getDb, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import { purgeStaleSearchRetrievals, startMemoryGc, stopMemoryGc } from "../http/memory";
|
|
5
|
+
|
|
6
|
+
const TEST_DB_PATH = "./test-memory-retrieval-purge.sqlite";
|
|
7
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
async function removeDbFiles(): Promise<void> {
|
|
10
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
11
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function insertRetrieval(id: string, eventType: string, ageDays: number): Promise<void> {
|
|
15
|
+
await getDbClient().run(
|
|
16
|
+
`INSERT INTO memory_retrieval (id, agentId, memoryId, retrievedAt, eventType)
|
|
17
|
+
VALUES (?, 'agent-1', 'memory-1', ?, ?)`,
|
|
18
|
+
[id, new Date(Date.now() - ageDays * DAY).toISOString(), eventType],
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("purgeStaleSearchRetrievals", () => {
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
await removeDbFiles();
|
|
25
|
+
initDb(TEST_DB_PATH);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
closeDb();
|
|
30
|
+
await removeDbFiles();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("deletes stale search rows in batches and keeps fresh and get rows", async () => {
|
|
34
|
+
for (let i = 0; i < 5; i++) await insertRetrieval(`stale-search-${i}`, "search", 120);
|
|
35
|
+
await insertRetrieval("fresh-search", "search", 10);
|
|
36
|
+
await insertRetrieval("stale-get", "get", 120);
|
|
37
|
+
|
|
38
|
+
const run = spyOn(getDbClient(), "run");
|
|
39
|
+
let purged: number;
|
|
40
|
+
let calls: (typeof run.mock.calls)[number][];
|
|
41
|
+
try {
|
|
42
|
+
purged = await purgeStaleSearchRetrievals(2);
|
|
43
|
+
calls = [...run.mock.calls];
|
|
44
|
+
} finally {
|
|
45
|
+
run.mockRestore();
|
|
46
|
+
}
|
|
47
|
+
expect(purged).toBe(5);
|
|
48
|
+
// Batches of 2: 2 + 2 + 1, and the short batch ends the loop.
|
|
49
|
+
expect(calls).toHaveLength(3);
|
|
50
|
+
|
|
51
|
+
const left = await getDbClient().query<{ id: string }>(
|
|
52
|
+
"SELECT id FROM memory_retrieval ORDER BY id",
|
|
53
|
+
);
|
|
54
|
+
expect(left.map((row) => row.id)).toEqual(["fresh-search", "stale-get"]);
|
|
55
|
+
|
|
56
|
+
// Without sqlite_stat1 the plan must stay on the retrievedAt range, not
|
|
57
|
+
// read every search row through the eventType index.
|
|
58
|
+
const [sql, params] = calls[0]!;
|
|
59
|
+
const plan = getDb()
|
|
60
|
+
.query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
|
|
61
|
+
.all(...((params ?? []) as any[]))
|
|
62
|
+
.map((row) => row.detail)
|
|
63
|
+
.join("\n");
|
|
64
|
+
expect(plan).toContain("USING INDEX idx_memret_retrieved_at (retrievedAt<?)");
|
|
65
|
+
expect(plan).not.toContain("idx_memret_event_type");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("startMemoryGc", () => {
|
|
70
|
+
test("defers the first purge off the boot path", async () => {
|
|
71
|
+
const timeout = spyOn(globalThis, "setTimeout");
|
|
72
|
+
try {
|
|
73
|
+
await startMemoryGc(60 * 60 * 1000, 120_000);
|
|
74
|
+
expect(timeout.mock.calls.some(([, ms]) => ms === 120_000)).toBe(true);
|
|
75
|
+
} finally {
|
|
76
|
+
stopMemoryGc();
|
|
77
|
+
timeout.mockRestore();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
4
|
+
import {
|
|
5
|
+
ControllerClientMessageSchema,
|
|
6
|
+
ControllerCommandSchema,
|
|
7
|
+
} from "../runtime-providers/controller-protocol";
|
|
8
|
+
|
|
9
|
+
test("migration 260 adds an optional JSON cache and the release table to a fresh database", () => {
|
|
10
|
+
const db = new Database(":memory:");
|
|
11
|
+
try {
|
|
12
|
+
runMigrations(db);
|
|
13
|
+
expect(db.query("PRAGMA table_info(runtime_pools)").all()).toContainEqual(
|
|
14
|
+
expect.objectContaining({ name: "cache", type: "TEXT", notnull: 0 }),
|
|
15
|
+
);
|
|
16
|
+
expect(db.query("PRAGMA foreign_key_check").all()).toEqual([]);
|
|
17
|
+
for (const name of ["runtime_ids", "state", "created_by", "updated_by"])
|
|
18
|
+
expect(db.query("PRAGMA table_info(runtime_cache_releases)").all()).toContainEqual(
|
|
19
|
+
expect.objectContaining({ name, type: "TEXT" }),
|
|
20
|
+
);
|
|
21
|
+
} finally {
|
|
22
|
+
db.close();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("migration 260 preserves existing pools with caching disabled", async () => {
|
|
27
|
+
const db = new Database(":memory:");
|
|
28
|
+
try {
|
|
29
|
+
db.exec(
|
|
30
|
+
"CREATE TABLE _migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)",
|
|
31
|
+
);
|
|
32
|
+
for (const file of [...new Bun.Glob("*.sql").scanSync("src/be/migrations")]
|
|
33
|
+
.sort()
|
|
34
|
+
.filter((file) => Number(file.split("_")[0]) <= 259)) {
|
|
35
|
+
const sql = await Bun.file(`src/be/migrations/${file}`).text();
|
|
36
|
+
db.transaction(() => {
|
|
37
|
+
db.exec(sql);
|
|
38
|
+
db.run("INSERT INTO _migrations VALUES (?, ?, ?, ?)", [
|
|
39
|
+
Number(file.split("_")[0]),
|
|
40
|
+
file.replace(".sql", ""),
|
|
41
|
+
new Date().toISOString(),
|
|
42
|
+
new Bun.CryptoHasher("sha256").update(sql).digest("hex"),
|
|
43
|
+
]);
|
|
44
|
+
}).immediate();
|
|
45
|
+
}
|
|
46
|
+
db.run(
|
|
47
|
+
"INSERT INTO runtime_pools (id, name, kind, placements, image, resources, env, lifecycle) VALUES (?, ?, ?, ?, ?, ?, ?, 'idle')",
|
|
48
|
+
[
|
|
49
|
+
"existing-pool",
|
|
50
|
+
"existing",
|
|
51
|
+
"docker",
|
|
52
|
+
'["controller"]',
|
|
53
|
+
"worker:test",
|
|
54
|
+
'{"cpu":2}',
|
|
55
|
+
'{"CUSTOM":"kept"}',
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
runMigrations(db);
|
|
59
|
+
expect(db.query("SELECT name, image, resources, env, cache FROM runtime_pools").get()).toEqual({
|
|
60
|
+
name: "existing",
|
|
61
|
+
image: "worker:test",
|
|
62
|
+
resources: '{"cpu":2}',
|
|
63
|
+
env: '{"CUSTOM":"kept"}',
|
|
64
|
+
cache: null,
|
|
65
|
+
});
|
|
66
|
+
expect(() => db.run("UPDATE runtime_pools SET cache = 'invalid json'")).toThrow();
|
|
67
|
+
expect(db.query("SELECT COUNT(*) AS n FROM runtime_cache_releases").get()).toEqual({ n: 0 });
|
|
68
|
+
} finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("cache release runtime IDs accept arrays and reject other JSON values", () => {
|
|
74
|
+
const db = new Database(":memory:");
|
|
75
|
+
try {
|
|
76
|
+
runMigrations(db);
|
|
77
|
+
db.run(
|
|
78
|
+
"INSERT INTO runtime_controllers (id, name, kind, placement) VALUES ('controller', 'controller', 'docker', 'builtin')",
|
|
79
|
+
);
|
|
80
|
+
db.run(`INSERT INTO runtime_cache_releases (id, pool_id, controller_id, owner_id, cache_key, runtime_ids)
|
|
81
|
+
VALUES ('release', 'pool', 'controller', 'owner', 'cache', '[]')`);
|
|
82
|
+
for (const invalid of ["null", "{}", "42", "true", '"runtime"', "invalid json"])
|
|
83
|
+
expect(() =>
|
|
84
|
+
db.run("UPDATE runtime_cache_releases SET runtime_ids = ?", [invalid]),
|
|
85
|
+
).toThrow();
|
|
86
|
+
db.run("UPDATE runtime_cache_releases SET runtime_ids = ?", ['["runtime-a","runtime-b"]']);
|
|
87
|
+
expect(db.query("SELECT runtime_ids FROM runtime_cache_releases").get()).toEqual({
|
|
88
|
+
runtime_ids: '["runtime-a","runtime-b"]',
|
|
89
|
+
});
|
|
90
|
+
} finally {
|
|
91
|
+
db.close();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("controller protocol accepts legacy hello messages and retains optional cache metadata", () => {
|
|
96
|
+
const legacy = ControllerClientMessageSchema.parse({
|
|
97
|
+
type: "hello",
|
|
98
|
+
controllerId: "controller",
|
|
99
|
+
kind: "docker",
|
|
100
|
+
version: 1,
|
|
101
|
+
traits: { canProvision: true, ephemeralDisk: true, supportsPause: false },
|
|
102
|
+
});
|
|
103
|
+
if (legacy.type !== "hello") throw new Error("Expected hello");
|
|
104
|
+
expect(legacy.traits.persistentCache).toBeUndefined();
|
|
105
|
+
const payload = {
|
|
106
|
+
runtimeId: "runtime",
|
|
107
|
+
agentId: "agent",
|
|
108
|
+
poolId: "pool",
|
|
109
|
+
ownerId: "owner",
|
|
110
|
+
image: "worker:test",
|
|
111
|
+
env: {},
|
|
112
|
+
labels: {},
|
|
113
|
+
cache: { key: "swarm-cache-pool", mountPath: "/cache" },
|
|
114
|
+
};
|
|
115
|
+
expect(
|
|
116
|
+
ControllerCommandSchema.parse({ type: "cmd", id: "command", op: "provision", payload }),
|
|
117
|
+
).toMatchObject({ payload: { cache: payload.cache } });
|
|
118
|
+
});
|
|
@@ -156,6 +156,22 @@ const CASES: Array<{
|
|
|
156
156
|
uses: ["COVERING INDEX idx_session_summaries_source (source=?)"],
|
|
157
157
|
revert: { dropIndex: "idx_session_summaries_source" },
|
|
158
158
|
},
|
|
159
|
+
{
|
|
160
|
+
// A user's task count hides other users' sessions. Both steps of that
|
|
161
|
+
// walk stay in an index (migration 262) instead of reading task rows.
|
|
162
|
+
name: "getTasksCount readableBy, root step",
|
|
163
|
+
match: /COUNT/,
|
|
164
|
+
run: () => getTasksCount({ status: "in_progress", readableBy: { userId: "viewer-1" } }),
|
|
165
|
+
uses: ["SEARCH r USING COVERING INDEX idx_agent_tasks_session_owner (scheduleId=?)"],
|
|
166
|
+
revert: { dropIndex: "idx_agent_tasks_session_owner" },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "getTasksCount readableBy, recursive step",
|
|
170
|
+
match: /COUNT/,
|
|
171
|
+
run: () => getTasksCount({ status: "in_progress", readableBy: { userId: "viewer-1" } }),
|
|
172
|
+
uses: ["SEARCH c USING COVERING INDEX idx_agent_tasks_parentTaskId_id (parentTaskId=?)"],
|
|
173
|
+
revert: { dropIndex: "idx_agent_tasks_parentTaskId_id" },
|
|
174
|
+
},
|
|
159
175
|
{
|
|
160
176
|
// Few hits: the page reads the agent_tasks_fts candidates by rowid. The
|
|
161
177
|
// control is the old `task LIKE '%q%'`, which scans every task body.
|
|
@@ -221,6 +237,17 @@ describe("query audit plans without sqlite_stat1", () => {
|
|
|
221
237
|
});
|
|
222
238
|
}
|
|
223
239
|
|
|
240
|
+
test("the default task count scans the filter index, not the task rows", async () => {
|
|
241
|
+
// The heartbeat filter is a NOT IN and a NOT LIKE, so the count scans.
|
|
242
|
+
// Migration 263 lets it scan the narrow index instead of every task body.
|
|
243
|
+
const count = await capturedSql(/COUNT/, () =>
|
|
244
|
+
getTasksCount({ readableBy: { userId: "viewer-1" } }),
|
|
245
|
+
);
|
|
246
|
+
const uses = ["SCAN agent_tasks USING COVERING INDEX idx_agent_tasks_list_filter"];
|
|
247
|
+
expect(missing(plan(count), uses)).toEqual([]);
|
|
248
|
+
expect(missing(planWithoutIndex(count, "idx_agent_tasks_list_filter"), uses)).not.toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
|
|
224
251
|
test("task search with many hits pages by lastUpdatedAt and counts from the index", async () => {
|
|
225
252
|
await getDbClient().transaction(async (tx) => {
|
|
226
253
|
for (let i = 0; i < 501; i++) {
|