@desplega.ai/agent-swarm 1.157.0 → 1.158.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
- package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
- package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
- package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
- package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
- package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
- package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
- package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
- package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
- package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
- package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
- package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
- package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
- package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
- package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
- package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
- package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
- package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
- package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
- package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
- package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
- package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
- package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
- package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
- package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
- package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
- package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
- package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
- package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
- package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
- package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
- package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
- package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
- package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
- package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
- package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
- package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
- package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
- package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
- package/dist/cli-t1ze64h3.js +811 -0
- package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
- package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
- package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
- package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
- package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
- package/dist/cli.js +29 -15
- package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
- package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
- package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
- package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
- package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
- package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
- package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
- package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
- package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
- package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
- package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
- package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
- package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
- package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
- package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
- package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
- package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
- package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
- package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
- package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
- package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
- package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
- package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
- package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
- package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
- package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
- package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
- package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
- package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
- package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
- package/openapi.json +1626 -629
- package/package.json +2 -1
- package/src/apps/catalog.generated.json +229 -0
- package/src/apps/page-validator.ts +71 -16
- package/src/apps/row-store.ts +66 -11
- package/src/apps/sync.ts +59 -17
- package/src/artifact-sdk/browser-sdk.ts +436 -2
- package/src/be/approval-sweeps.ts +109 -0
- package/src/be/boot-seeds.ts +40 -0
- package/src/be/chunking.ts +1 -1
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/approvals.ts +78 -0
- package/src/be/db/tasks/read.ts +197 -39
- package/src/be/db/tasks/write.ts +41 -9
- package/src/be/db-client.ts +107 -48
- package/src/be/db.ts +219 -76
- package/src/be/events.ts +33 -1
- package/src/be/memory/graph-expansion.ts +10 -3
- package/src/be/memory/providers/sqlite-store.ts +55 -13
- package/src/be/memory/raters/retrieval.ts +5 -2
- package/src/be/memory/reranker.ts +26 -2
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
- package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
- package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
- package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +1857 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-normalize.ts +2 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +53 -3
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/task-citations.ts +29 -0
- package/src/cli.tsx +14 -1
- package/src/commands/onboard/compose-generator.ts +8 -3
- package/src/commands/onboard/container-engine.ts +220 -0
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/health-check.tsx +4 -1
- package/src/commands/onboard/steps/prereq-check.tsx +50 -54
- package/src/commands/onboard/steps/start.tsx +31 -16
- package/src/commands/onboard/types.ts +7 -2
- package/src/commands/onboard.tsx +39 -4
- package/src/commands/profile-sync.ts +79 -26
- package/src/commands/runner.ts +8 -1
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +263 -103
- package/src/hooks/hook.ts +21 -12
- package/src/http/agents.ts +35 -0
- package/src/http/approval-requests.ts +136 -64
- package/src/http/apps.ts +14 -3
- package/src/http/config.ts +15 -1
- package/src/http/events.ts +62 -2
- package/src/http/index.ts +6 -4
- package/src/http/memory.ts +21 -7
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +9 -4
- package/src/http/sessions.ts +25 -18
- package/src/http/stats.ts +19 -7
- package/src/http/status.ts +11 -1
- package/src/http/tasks.ts +40 -8
- package/src/http/webhooks.ts +67 -46
- package/src/http/workflows.ts +15 -20
- package/src/linear/outbound.ts +37 -15
- package/src/otel-impl.ts +151 -1
- package/src/otel.ts +14 -3
- package/src/prompts/memories.ts +19 -2
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/modelsdev-reasoning.json +551 -0
- package/src/rbac/legacy-policy.ts +16 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/schedule-task.ts +3 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +12 -7
- package/src/slack/app.ts +35 -4
- package/src/slack/connection-state.ts +17 -0
- package/src/tasks/context-key.ts +1 -0
- package/src/tasks/worker-follow-up.ts +6 -2
- package/src/tests/agent-task-activity.test.ts +74 -0
- package/src/tests/agents-list-model-display.test.ts +6 -1
- package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
- package/src/tests/approval-requests.test.ts +395 -0
- package/src/tests/approval-sweeps-config.test.ts +47 -0
- package/src/tests/approval-sweeps.test.ts +508 -0
- package/src/tests/apps-kibo-blocks.test.ts +77 -0
- package/src/tests/apps-spike.test.ts +56 -1
- package/src/tests/apps-sync-engine.test.ts +20 -9
- package/src/tests/boot-seeds.test.ts +170 -0
- package/src/tests/cancel-approval-request-tool.test.ts +115 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/config-key-filter.test.ts +80 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-client-busy-retry.test.ts +96 -0
- package/src/tests/db-client.test.ts +60 -1
- package/src/tests/db-task-read-contract.test.ts +10 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/events-http.test.ts +103 -0
- package/src/tests/extensions-catalog.test.ts +22 -17
- package/src/tests/extensions-pre-task.test.ts +107 -0
- package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
- package/src/tests/follow-up-config-inheritance.test.ts +260 -0
- package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
- package/src/tests/heartbeat-reboot-live.test.ts +95 -0
- package/src/tests/heartbeat-stall-cas.test.ts +80 -0
- package/src/tests/heartbeat.test.ts +99 -3
- package/src/tests/hook-mcp-config.test.ts +29 -0
- package/src/tests/http-api-integration.test.ts +16 -2
- package/src/tests/kv-storage.test.ts +62 -0
- package/src/tests/linear-outbound-sync.test.ts +120 -5
- package/src/tests/list-endpoint-slimming.test.ts +91 -0
- package/src/tests/memory-graph-organic-route.test.ts +155 -0
- package/src/tests/memory-http-recall-gating.test.ts +14 -4
- package/src/tests/memory-hybrid.test.ts +161 -1
- package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
- package/src/tests/memory.test.ts +22 -0
- package/src/tests/onboard-container-engine.test.ts +240 -0
- package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
- package/src/tests/pricing-routes.test.ts +14 -0
- package/src/tests/profile-sync-warning.test.ts +8 -4
- package/src/tests/promote-draft-task-route.test.ts +19 -0
- package/src/tests/provider-registration.test.ts +296 -42
- package/src/tests/rbac-engine.test.ts +30 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/script-connections.test.ts +7 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions-detail-batch.test.ts +250 -0
- package/src/tests/sessions.test.ts +118 -0
- package/src/tests/slack-config.test.ts +22 -0
- package/src/tests/sonnet-5-5-registry.test.ts +44 -0
- package/src/tests/status.test.ts +36 -1
- package/src/tests/swarm-diff.test.ts +316 -10
- package/src/tests/task-completion-idempotency.test.ts +2 -1
- package/src/tests/tasks-requested-by-filter.test.ts +3 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/ui-list-slim-projections.test.ts +158 -0
- package/src/tests/webhooks-task-blocked.test.ts +127 -0
- package/src/tests/workflow-approval-recovery.test.ts +203 -0
- package/src/tests/workflow-hitl-routing.test.ts +22 -1
- package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
- package/src/tests/workflow-http-v2.test.ts +99 -8
- package/src/tests/workflow-retry-validation.test.ts +56 -0
- package/src/tests/workflow-tla-races.test.ts +389 -0
- package/src/tests/workflow-validation-port-routing.test.ts +113 -1
- package/src/tools/cancel-approval-request.ts +57 -0
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/request-human-input.ts +4 -2
- package/src/tools/send-task.ts +4 -0
- package/src/tools/templates.ts +19 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/workflows/list-workflow-runs.ts +1 -0
- package/src/types.ts +38 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/approval-cancel.ts +152 -0
- package/src/workflows/approval-notifications.ts +84 -0
- package/src/workflows/checkpoint.ts +10 -5
- package/src/workflows/definition.ts +21 -0
- package/src/workflows/engine.ts +152 -21
- package/src/workflows/executors/human-in-the-loop.ts +1 -1
- package/src/workflows/recovery.ts +100 -45
- package/src/workflows/resume.ts +49 -18
- package/src/workflows/retry-poller.ts +71 -21
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/github-sender-allowlist/README.md +18 -0
- package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
- package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
- package/templates/skills/apps/content.md +20 -1
- package/templates/skills/code-quality/content.md +26 -1
- package/templates/skills/pages/content.md +16 -4
- package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
- package/dist/cli-2daksqps.js +0 -378
- package/dist/rbac-roles-3gxmnb20.js +0 -37
|
@@ -573,7 +573,8 @@ describe("Tasks", () => {
|
|
|
573
573
|
expect(status).toBe(200);
|
|
574
574
|
expect(body.tasks).toBeDefined();
|
|
575
575
|
expect(Array.isArray(body.tasks)).toBe(true);
|
|
576
|
-
|
|
576
|
+
// `total` is an opt-in COUNT(*): omitted unless `includeTotal=true`.
|
|
577
|
+
expect(body.total).toBeUndefined();
|
|
577
578
|
});
|
|
578
579
|
|
|
579
580
|
test("GET /api/tasks?status=pending — filter by status", async () => {
|
|
@@ -599,12 +600,25 @@ describe("Tasks", () => {
|
|
|
599
600
|
});
|
|
600
601
|
|
|
601
602
|
test("GET /api/tasks?limit=1 — pagination", async () => {
|
|
602
|
-
const { status, body } = await get("/api/tasks?limit=1");
|
|
603
|
+
const { status, body } = await get("/api/tasks?limit=1&includeTotal=true");
|
|
603
604
|
expect(status).toBe(200);
|
|
604
605
|
expect(body.tasks.length).toBe(1);
|
|
605
606
|
expect(body.total).toBeGreaterThanOrEqual(2);
|
|
606
607
|
});
|
|
607
608
|
|
|
609
|
+
test("GET /api/tasks?fields=timeline — narrow timeline rows", async () => {
|
|
610
|
+
const { status, body } = await get("/api/tasks?fields=timeline&orderBy=createdAt&limit=5");
|
|
611
|
+
expect(status).toBe(200);
|
|
612
|
+
expect(body.tasks.length).toBeGreaterThanOrEqual(1);
|
|
613
|
+
for (const t of body.tasks) {
|
|
614
|
+
expect(typeof t.id).toBe("string");
|
|
615
|
+
expect(typeof t.status).toBe("string");
|
|
616
|
+
expect(t.progress).toBeUndefined();
|
|
617
|
+
expect(t.tags).toBeUndefined();
|
|
618
|
+
}
|
|
619
|
+
expect(body.total).toBeUndefined();
|
|
620
|
+
});
|
|
621
|
+
|
|
608
622
|
test("GET /api/tasks/:id — get specific task", async () => {
|
|
609
623
|
const { status, body } = await get(`/api/tasks/${ids.task}`);
|
|
610
624
|
expect(status).toBe(200);
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
getKv,
|
|
10
10
|
incrKv,
|
|
11
11
|
initDb,
|
|
12
|
+
type KvJsonFieldEquals,
|
|
12
13
|
KvTypeCollisionError,
|
|
14
|
+
kvPrefixUpperBound,
|
|
13
15
|
listKv,
|
|
14
16
|
sweepExpiredKv,
|
|
15
17
|
sweepExpiredKvPrefix,
|
|
@@ -200,6 +202,66 @@ describe("kv-storage helpers", () => {
|
|
|
200
202
|
expect(exact.map((e) => e.key)).toEqual(["x_1"]);
|
|
201
203
|
});
|
|
202
204
|
|
|
205
|
+
test("listKv prefix is an exact, case-sensitive range on the primary key", async () => {
|
|
206
|
+
for (const key of ["m/row/1", "m/row/2", "M/row/3", "m/rowx", "m/row", "n/row/1", "m/row/é"]) {
|
|
207
|
+
await upsertKv({ namespace: NS, key, value: 1, valueType: "integer" });
|
|
208
|
+
}
|
|
209
|
+
const rows = await listKv(NS, { prefix: "m/row/", limit: 100, offset: 0 });
|
|
210
|
+
expect(rows.map((e) => e.key)).toEqual(["m/row/1", "m/row/2", "m/row/é"]);
|
|
211
|
+
expect(await countKv(NS, { prefix: "m/row/" })).toBe(3);
|
|
212
|
+
expect(kvPrefixUpperBound("m/row/")).toBe("m/row0");
|
|
213
|
+
expect(kvPrefixUpperBound(`a${String.fromCodePoint(0x10ffff)}`)).toBe("b");
|
|
214
|
+
expect(kvPrefixUpperBound(String.fromCodePoint(0xd7ff))).toBe(String.fromCodePoint(0xe000));
|
|
215
|
+
|
|
216
|
+
const plan = await getDbClient().query<{ detail: string }>(
|
|
217
|
+
`EXPLAIN QUERY PLAN SELECT key FROM kv_entries
|
|
218
|
+
WHERE namespace = ? AND (expires_at IS NULL OR expires_at > ?) AND key >= ? AND key < ?`,
|
|
219
|
+
[NS, 0, "m/row/", "m/row0"],
|
|
220
|
+
);
|
|
221
|
+
expect(plan.map((row) => row.detail).join(" ")).toContain("key>? AND key<?");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("listKv jsonFieldEquals narrows rows in SQL and skips corrupt JSON", async () => {
|
|
225
|
+
await upsertKv({
|
|
226
|
+
namespace: NS,
|
|
227
|
+
key: "r/1",
|
|
228
|
+
value: { run: "a", ok: true, n: 2 },
|
|
229
|
+
valueType: "json",
|
|
230
|
+
});
|
|
231
|
+
await upsertKv({
|
|
232
|
+
namespace: NS,
|
|
233
|
+
key: "r/2",
|
|
234
|
+
value: { run: "b", ok: false, n: 2 },
|
|
235
|
+
valueType: "json",
|
|
236
|
+
});
|
|
237
|
+
await upsertKv({
|
|
238
|
+
namespace: NS,
|
|
239
|
+
key: "r/3",
|
|
240
|
+
value: { run: "a", ok: false, n: 3.5 },
|
|
241
|
+
valueType: "json",
|
|
242
|
+
});
|
|
243
|
+
await getDbClient().run(
|
|
244
|
+
"INSERT INTO kv_entries (namespace, key, value, value_type) VALUES (?, ?, ?, 'json')",
|
|
245
|
+
[NS, "r/4", "{not json"],
|
|
246
|
+
);
|
|
247
|
+
const keys = async (jsonFieldEquals: KvJsonFieldEquals[]) =>
|
|
248
|
+
(await listKv(NS, { prefix: "r/", limit: 100, offset: 0, jsonFieldEquals })).map(
|
|
249
|
+
(e) => e.key,
|
|
250
|
+
);
|
|
251
|
+
expect(await keys([])).toEqual(["r/1", "r/2", "r/3", "r/4"]);
|
|
252
|
+
expect(await keys([{ field: "run", value: "a" }])).toEqual(["r/1", "r/3"]);
|
|
253
|
+
expect(await keys([{ field: "ok", value: true }])).toEqual(["r/1"]);
|
|
254
|
+
// Non-safe-integer numbers are not prefiltered (the caller re-checks).
|
|
255
|
+
expect(await keys([{ field: "n", value: 3.5 }])).toEqual(["r/1", "r/2", "r/3", "r/4"]);
|
|
256
|
+
expect(
|
|
257
|
+
await keys([
|
|
258
|
+
{ field: "run", value: "a" },
|
|
259
|
+
{ field: "n", value: 2 },
|
|
260
|
+
]),
|
|
261
|
+
).toEqual(["r/1"]);
|
|
262
|
+
expect(await keys([{ field: "missing", value: "a" }])).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
203
265
|
test("incrKv creates from missing", async () => {
|
|
204
266
|
const entry = await incrKv(NS, "counter", 3);
|
|
205
267
|
expect(entry.value).toBe(3);
|
|
@@ -2,7 +2,12 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, tes
|
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, initDb } from "../be/db";
|
|
4
4
|
import { createTrackerSync, getTrackerSync, updateTrackerSync } from "../be/db-queries/tracker";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatLinearOutcomeBody,
|
|
7
|
+
initLinearOutboundSync,
|
|
8
|
+
LINEAR_BODY_MAX,
|
|
9
|
+
teardownLinearOutboundSync,
|
|
10
|
+
} from "../linear/outbound";
|
|
6
11
|
import { taskSessionMap } from "../linear/sync";
|
|
7
12
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
8
13
|
|
|
@@ -79,8 +84,8 @@ describe("Linear Outbound Sync", () => {
|
|
|
79
84
|
const callArgs = mockCreateComment.mock.calls[0] as unknown[];
|
|
80
85
|
const arg = callArgs[0] as { issueId: string; body: string };
|
|
81
86
|
expect(arg.issueId).toBe("LIN-OUT-COMPLETED");
|
|
82
|
-
|
|
83
|
-
expect(arg.body).
|
|
87
|
+
// The output is the whole comment: no boilerplate, no collapsed `+++` section.
|
|
88
|
+
expect(arg.body).toBe("All done!");
|
|
84
89
|
|
|
85
90
|
// Verify sync record updated
|
|
86
91
|
const updated = await getTrackerSync("linear", "task", "outbound-task-completed");
|
|
@@ -107,8 +112,118 @@ describe("Linear Outbound Sync", () => {
|
|
|
107
112
|
const callArgs = mockCreateComment.mock.calls[0] as unknown[];
|
|
108
113
|
const arg = callArgs[0] as { issueId: string; body: string };
|
|
109
114
|
expect(arg.issueId).toBe("LIN-OUT-FAILED");
|
|
110
|
-
expect(arg.body).
|
|
111
|
-
|
|
115
|
+
expect(arg.body).toBe("Task failed.\n\nBuild error in module X");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("task.completed with an AgentSession posts ONE response activity carrying the full output", async () => {
|
|
119
|
+
const taskId = "outbound-session-completed";
|
|
120
|
+
await createTrackerSync({
|
|
121
|
+
provider: "linear",
|
|
122
|
+
entityType: "task",
|
|
123
|
+
swarmId: taskId,
|
|
124
|
+
externalId: "LIN-OUT-SESSION-COMPLETED",
|
|
125
|
+
syncDirection: "bidirectional",
|
|
126
|
+
});
|
|
127
|
+
taskSessionMap.set(taskId, "session-completed");
|
|
128
|
+
// Longer than the old 2000-char cut, shorter than the Linear body cap.
|
|
129
|
+
const output = `## Answer\n\n${"x".repeat(5000)}`;
|
|
130
|
+
|
|
131
|
+
workflowEventBus.emit("task.completed", { taskId, output });
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
133
|
+
|
|
134
|
+
expect(mockEndAgentSession).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(mockEndAgentSession.mock.calls[0] as unknown[]).toEqual([
|
|
136
|
+
"session-completed",
|
|
137
|
+
output,
|
|
138
|
+
"response",
|
|
139
|
+
]);
|
|
140
|
+
expect(mockCreateComment).not.toHaveBeenCalled();
|
|
141
|
+
expect(taskSessionMap.has(taskId)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("task.failed with an AgentSession posts ONE error activity with the plain reason", async () => {
|
|
145
|
+
const taskId = "outbound-session-failed";
|
|
146
|
+
await createTrackerSync({
|
|
147
|
+
provider: "linear",
|
|
148
|
+
entityType: "task",
|
|
149
|
+
swarmId: taskId,
|
|
150
|
+
externalId: "LIN-OUT-SESSION-FAILED",
|
|
151
|
+
syncDirection: "bidirectional",
|
|
152
|
+
});
|
|
153
|
+
taskSessionMap.set(taskId, "session-failed");
|
|
154
|
+
|
|
155
|
+
workflowEventBus.emit("task.failed", { taskId, failureReason: "tsc:check failed" });
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
157
|
+
|
|
158
|
+
expect(mockEndAgentSession).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(mockEndAgentSession.mock.calls[0] as unknown[]).toEqual([
|
|
160
|
+
"session-failed",
|
|
161
|
+
"Task failed.\n\ntsc:check failed",
|
|
162
|
+
"error",
|
|
163
|
+
]);
|
|
164
|
+
expect(mockCreateComment).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("task.completed with no output still ends the session with a short body", async () => {
|
|
168
|
+
const taskId = "outbound-session-empty";
|
|
169
|
+
await createTrackerSync({
|
|
170
|
+
provider: "linear",
|
|
171
|
+
entityType: "task",
|
|
172
|
+
swarmId: taskId,
|
|
173
|
+
externalId: "LIN-OUT-SESSION-EMPTY",
|
|
174
|
+
syncDirection: "bidirectional",
|
|
175
|
+
});
|
|
176
|
+
taskSessionMap.set(taskId, "session-empty");
|
|
177
|
+
|
|
178
|
+
workflowEventBus.emit("task.completed", { taskId, output: " " });
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
180
|
+
|
|
181
|
+
expect(mockEndAgentSession.mock.calls[0] as unknown[]).toEqual([
|
|
182
|
+
"session-empty",
|
|
183
|
+
"Task completed.",
|
|
184
|
+
"response",
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("formatLinearOutcomeBody", () => {
|
|
189
|
+
const savedDashboard = process.env.SWARM_DASHBOARD_URL;
|
|
190
|
+
const savedApp = process.env.APP_URL;
|
|
191
|
+
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
if (savedDashboard === undefined) delete process.env.SWARM_DASHBOARD_URL;
|
|
194
|
+
else process.env.SWARM_DASHBOARD_URL = savedDashboard;
|
|
195
|
+
if (savedApp === undefined) delete process.env.APP_URL;
|
|
196
|
+
else process.env.APP_URL = savedApp;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("returns text at the cap unchanged", () => {
|
|
200
|
+
const text = "a".repeat(LINEAR_BODY_MAX);
|
|
201
|
+
expect(formatLinearOutcomeBody(text, "t1")).toBe(text);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("caps oversized text with a visible note linking the swarm task", () => {
|
|
205
|
+
delete process.env.SWARM_DASHBOARD_URL;
|
|
206
|
+
process.env.APP_URL = "https://swarm.example.com/";
|
|
207
|
+
const text = "b".repeat(LINEAR_BODY_MAX + 500);
|
|
208
|
+
|
|
209
|
+
const body = formatLinearOutcomeBody(text, "task-123");
|
|
210
|
+
|
|
211
|
+
expect(body.length).toBeLessThanOrEqual(LINEAR_BODY_MAX);
|
|
212
|
+
expect(body.startsWith("b".repeat(1000))).toBe(true);
|
|
213
|
+
expect(body).toContain(`of ${text.length} characters`);
|
|
214
|
+
expect(body.endsWith("Full text: https://swarm.example.com/tasks/task-123_")).toBe(true);
|
|
215
|
+
expect(body).not.toContain("+++");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("names the task id when no dashboard URL is configured", () => {
|
|
219
|
+
delete process.env.SWARM_DASHBOARD_URL;
|
|
220
|
+
delete process.env.APP_URL;
|
|
221
|
+
|
|
222
|
+
const body = formatLinearOutcomeBody("c".repeat(LINEAR_BODY_MAX + 1), "task-456");
|
|
223
|
+
|
|
224
|
+
expect(body.length).toBeLessThanOrEqual(LINEAR_BODY_MAX);
|
|
225
|
+
expect(body.endsWith("Full text: swarm task `task-456`_")).toBe(true);
|
|
226
|
+
});
|
|
112
227
|
});
|
|
113
228
|
|
|
114
229
|
test("no-op when no tracker_sync mapping exists", async () => {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getAgentById,
|
|
12
12
|
getAllAgents,
|
|
13
13
|
getAllTasks,
|
|
14
|
+
getDbClient,
|
|
14
15
|
getScheduledTasks,
|
|
15
16
|
initDb,
|
|
16
17
|
listAllPages,
|
|
@@ -227,6 +228,96 @@ describe("list-endpoint slimming", () => {
|
|
|
227
228
|
expect(full?.totalCostUsd).toBeCloseTo(0.0168, 6);
|
|
228
229
|
});
|
|
229
230
|
|
|
231
|
+
test("getAllTasks — SQL-side preview matches truncating the full text", async () => {
|
|
232
|
+
// The SQL cut counts UTF-8 bytes while JS slices UTF-16 units, so pin the
|
|
233
|
+
// boundary with astral (surrogate-pair) and multibyte characters. SQLite's
|
|
234
|
+
// text functions stop at an embedded NUL, so pin that too, plus a leading
|
|
235
|
+
// BOM a default TextDecoder would strip. The BOM goes in as SQL bytes:
|
|
236
|
+
// bun:sqlite drops a leading BOM when it binds a string.
|
|
237
|
+
const agentId = "preview-parity-agent";
|
|
238
|
+
const marker = "preview-parity-";
|
|
239
|
+
const texts = [
|
|
240
|
+
`${marker}${"a".repeat(285)}`, // exactly 300 units
|
|
241
|
+
`${marker}${"a".repeat(286)}`, // 301 units
|
|
242
|
+
`${marker}${"😀".repeat(200)}`,
|
|
243
|
+
`${marker}${"é".repeat(284)}😀`,
|
|
244
|
+
`${marker}${"ñ".repeat(500)}`,
|
|
245
|
+
`${marker}short`,
|
|
246
|
+
"a\u0000b",
|
|
247
|
+
`\u0000${"x".repeat(350)}`,
|
|
248
|
+
];
|
|
249
|
+
for (const text of texts) await createTaskExtended(text, { agentId });
|
|
250
|
+
const bom = await createTaskExtended(`${marker}bom`, { agentId });
|
|
251
|
+
texts.push(`\uFEFF${marker}bom`);
|
|
252
|
+
await getDbClient().run(
|
|
253
|
+
"UPDATE agent_tasks SET task = CAST(x'EFBBBF' AS TEXT) || task WHERE id = ?",
|
|
254
|
+
[bom.id],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const full = await getAllTasks({ agentId, limit: 50 });
|
|
258
|
+
expect(full.map((t) => t.task).sort()).toEqual([...texts].sort());
|
|
259
|
+
for (const opts of [{ slim: true }, { fields: "timeline" as const }]) {
|
|
260
|
+
const rows = await getAllTasks({ agentId, limit: 50 }, opts);
|
|
261
|
+
expect(rows.map((t) => t.id)).toEqual(full.map((t) => t.id));
|
|
262
|
+
for (const [i, row] of full.entries()) {
|
|
263
|
+
const expected = row.task.length > 300 ? `${row.task.slice(0, 300)}…` : row.task;
|
|
264
|
+
expect(rows[i]!.task).toBe(expected);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("getAllTasks — slim and full pages hold the same rows in the same order", async () => {
|
|
270
|
+
for (let i = 0; i < 12; i++) {
|
|
271
|
+
await createTaskExtended(`page-parity ${i}`, { agentId: "slim-agent-1" });
|
|
272
|
+
}
|
|
273
|
+
for (const orderBy of ["lastUpdatedAt", "createdAt"] as const) {
|
|
274
|
+
for (const offset of [0, 5, 10]) {
|
|
275
|
+
const filters = { search: "page-parity", limit: 5, offset, orderBy };
|
|
276
|
+
const slimIds = (await getAllTasks(filters, { slim: true })).map((t) => t.id);
|
|
277
|
+
const fullIds = (await getAllTasks(filters)).map((t) => t.id);
|
|
278
|
+
const timelineIds = (await getAllTasks(filters, { fields: "timeline" })).map((t) => t.id);
|
|
279
|
+
expect(slimIds).toEqual(fullIds);
|
|
280
|
+
expect(timelineIds).toEqual(fullIds);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("getAllTasks — timeline projection carries only what the timeline draws", async () => {
|
|
286
|
+
const task = await createTaskExtended(`timeline-row ${"T".repeat(1000)}`, {
|
|
287
|
+
agentId: "slim-agent-1",
|
|
288
|
+
tags: ["timeline"],
|
|
289
|
+
});
|
|
290
|
+
await createSessionCost({
|
|
291
|
+
sessionId: "timeline-cost-session",
|
|
292
|
+
taskId: task.id,
|
|
293
|
+
agentId: "slim-agent-1",
|
|
294
|
+
totalCostUsd: 0.25,
|
|
295
|
+
durationMs: 1000,
|
|
296
|
+
numTurns: 1,
|
|
297
|
+
model: "test-model",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const [row] = await getAllTasks({ search: "timeline-row", limit: 1 }, { fields: "timeline" });
|
|
301
|
+
expect(Object.keys(row!).sort()).toEqual(
|
|
302
|
+
[
|
|
303
|
+
"agentId",
|
|
304
|
+
"createdAt",
|
|
305
|
+
"finishedAt",
|
|
306
|
+
"id",
|
|
307
|
+
"lastUpdatedAt",
|
|
308
|
+
"parentTaskId",
|
|
309
|
+
"peakContextTokens",
|
|
310
|
+
"status",
|
|
311
|
+
"task",
|
|
312
|
+
"title",
|
|
313
|
+
"totalCostUsd",
|
|
314
|
+
].sort(),
|
|
315
|
+
);
|
|
316
|
+
expect(row!.id).toBe(task.id);
|
|
317
|
+
expect(row!.task).toBe(`${task.task.slice(0, 300)}…`);
|
|
318
|
+
expect(row!.totalCostUsd).toBeCloseTo(0.25, 6);
|
|
319
|
+
});
|
|
320
|
+
|
|
230
321
|
test("listRecentSessions — slim root is a truncated task summary", async () => {
|
|
231
322
|
const longText = "Q".repeat(2000);
|
|
232
323
|
await createTaskExtended(longText, { agentId: "slim-agent-1" });
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
5
|
+
import { expandCandidatesWithGraph } from "../be/memory/graph-expansion";
|
|
6
|
+
import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
7
|
+
import { computeScore, rerank } from "../be/memory/reranker";
|
|
8
|
+
|
|
9
|
+
// PR #1689 review on 2e8c2f24: expansion replaced an organic duplicate with
|
|
10
|
+
// its graph route on the UNCAPPED graph score, then rerank() capped that route
|
|
11
|
+
// at a low-quality parent's composite. The neighbour's stronger organic match
|
|
12
|
+
// was gone. Fresh rows, default boosts, link strength 1, vector-only arm so
|
|
13
|
+
// each similarity is the fixture cosine:
|
|
14
|
+
// - task_completion parent: relevance 0.90, composite 0.90 x 0.7 = 0.63
|
|
15
|
+
// - manual neighbour, also a direct match: relevance 0.60, composite 0.90
|
|
16
|
+
// - unrelated manual row: relevance 0.54, composite 0.81
|
|
17
|
+
|
|
18
|
+
const TEST_DB_PATH = "./test-memory-graph-organic-route.sqlite";
|
|
19
|
+
const agentId = randomUUID();
|
|
20
|
+
|
|
21
|
+
/** Unit vector: `cos` on axis 2 (the query axis), the remainder on `axis`. */
|
|
22
|
+
function mix(cos: number, axis: number): Float32Array {
|
|
23
|
+
const embedding = new Float32Array(512);
|
|
24
|
+
embedding[2] = cos;
|
|
25
|
+
embedding[axis] = Math.sqrt(1 - cos * cos);
|
|
26
|
+
return embedding;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const queryEmbedding = mix(1, 3);
|
|
30
|
+
|
|
31
|
+
let parentId: string;
|
|
32
|
+
let neighborId: string;
|
|
33
|
+
let otherId: string;
|
|
34
|
+
let prevHybridFlag: string | undefined;
|
|
35
|
+
let prevGraphFlag: string | undefined;
|
|
36
|
+
|
|
37
|
+
async function searchAndExpand() {
|
|
38
|
+
const candidates = await new SqliteMemoryStore().search(queryEmbedding, agentId, {
|
|
39
|
+
scope: "all",
|
|
40
|
+
limit: 15,
|
|
41
|
+
});
|
|
42
|
+
const expanded = await expandCandidatesWithGraph(candidates, agentId, { scope: "all" });
|
|
43
|
+
return { candidates, expanded };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
prevHybridFlag = process.env.MEMORY_HYBRID_SEARCH;
|
|
48
|
+
prevGraphFlag = process.env.MEMORY_GRAPH_EXPANSION;
|
|
49
|
+
process.env.MEMORY_HYBRID_SEARCH = "0";
|
|
50
|
+
delete process.env.MEMORY_GRAPH_EXPANSION;
|
|
51
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
52
|
+
try {
|
|
53
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
initDb(TEST_DB_PATH);
|
|
57
|
+
await createAgent({ id: agentId, name: "Organic Route Agent", isLead: false, status: "idle" });
|
|
58
|
+
|
|
59
|
+
const store = new SqliteMemoryStore();
|
|
60
|
+
const parent = await store.store({
|
|
61
|
+
agentId,
|
|
62
|
+
scope: "agent",
|
|
63
|
+
name: "completed task summary",
|
|
64
|
+
content: "What the task did.",
|
|
65
|
+
source: "task_completion",
|
|
66
|
+
});
|
|
67
|
+
parentId = parent.id;
|
|
68
|
+
await store.updateEmbedding(parentId, mix(0.9, 4), "test");
|
|
69
|
+
|
|
70
|
+
const neighbor = await store.store({
|
|
71
|
+
agentId,
|
|
72
|
+
scope: "agent",
|
|
73
|
+
name: "manual note linked from the summary",
|
|
74
|
+
content: "A manual note that also matches the query directly.",
|
|
75
|
+
source: "manual",
|
|
76
|
+
});
|
|
77
|
+
neighborId = neighbor.id;
|
|
78
|
+
await store.updateEmbedding(neighborId, mix(0.6, 5), "test");
|
|
79
|
+
|
|
80
|
+
const other = await store.store({
|
|
81
|
+
agentId,
|
|
82
|
+
scope: "agent",
|
|
83
|
+
name: "unrelated manual note",
|
|
84
|
+
content: "Another manual note.",
|
|
85
|
+
source: "manual",
|
|
86
|
+
});
|
|
87
|
+
otherId = other.id;
|
|
88
|
+
await store.updateEmbedding(otherId, mix(0.54, 6), "test");
|
|
89
|
+
|
|
90
|
+
const nowIso = new Date().toISOString();
|
|
91
|
+
await getDbClient().run(
|
|
92
|
+
`INSERT INTO memory_link
|
|
93
|
+
(id, from_memory_id, linkType, targetKind, targetId, strength, resolver, sourceText, metadata, createdAt, updatedAt)
|
|
94
|
+
VALUES (?, ?, 'wikilink', 'memory', ?, 1.0, 'wikilink', 'note', NULL, ?, ?)`,
|
|
95
|
+
[randomUUID(), parentId, neighborId, nowIso, nowIso],
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterAll(async () => {
|
|
100
|
+
if (prevHybridFlag === undefined) delete process.env.MEMORY_HYBRID_SEARCH;
|
|
101
|
+
else process.env.MEMORY_HYBRID_SEARCH = prevHybridFlag;
|
|
102
|
+
if (prevGraphFlag === undefined) delete process.env.MEMORY_GRAPH_EXPANSION;
|
|
103
|
+
else process.env.MEMORY_GRAPH_EXPANSION = prevGraphFlag;
|
|
104
|
+
closeDb();
|
|
105
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
106
|
+
try {
|
|
107
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("graph route of a neighbour that already matches organically", () => {
|
|
113
|
+
test("fixture reproduces the review: the uncapped graph route beats the organic one, the capped route does not", async () => {
|
|
114
|
+
const { candidates } = await searchAndExpand();
|
|
115
|
+
const now = new Date();
|
|
116
|
+
const organic = candidates.find((c) => c.id === neighborId)!;
|
|
117
|
+
const parent = candidates.find((c) => c.id === parentId)!;
|
|
118
|
+
expect(organic.retrievalSource).toBe("vec");
|
|
119
|
+
expect(parent.retrievalSource).toBe("vec");
|
|
120
|
+
expect(candidates.find((c) => c.id === otherId)?.retrievalSource).toBe("vec");
|
|
121
|
+
|
|
122
|
+
const graphRoute = { ...organic, similarity: 0.9 * 0.7, retrievalSource: "graph" as const };
|
|
123
|
+
const organicScore = computeScore(organic, now);
|
|
124
|
+
const parentScore = computeScore(parent, now);
|
|
125
|
+
expect(organicScore).toBeCloseTo(0.9, 2);
|
|
126
|
+
expect(parentScore).toBeCloseTo(0.63, 2);
|
|
127
|
+
expect(computeScore(graphRoute, now)).toBeGreaterThan(organicScore);
|
|
128
|
+
expect(Math.min(computeScore(graphRoute, now), parentScore)).toBeLessThan(organicScore);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a low-quality parent's link does not demote a neighbour's stronger organic match", async () => {
|
|
132
|
+
const { expanded } = await searchAndExpand();
|
|
133
|
+
const neighbor = expanded.filter((c) => c.id === neighborId);
|
|
134
|
+
expect(neighbor).toHaveLength(1);
|
|
135
|
+
expect(neighbor[0]!.retrievalSource).toBe("vec");
|
|
136
|
+
expect(neighbor[0]!.graphParentId).toBeUndefined();
|
|
137
|
+
|
|
138
|
+
const [top] = rerank(expanded, { limit: 1 });
|
|
139
|
+
expect(top!.id).toBe(neighborId);
|
|
140
|
+
expect(top!.retrievalSource).toBe("vec");
|
|
141
|
+
expect(top!.compositeScore!).toBeCloseTo(0.9, 2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("the result matches search without graph expansion", async () => {
|
|
145
|
+
process.env.MEMORY_GRAPH_EXPANSION = "0";
|
|
146
|
+
try {
|
|
147
|
+
const { expanded } = await searchAndExpand();
|
|
148
|
+
const [top] = rerank(expanded, { limit: 1 });
|
|
149
|
+
expect(top!.id).toBe(neighborId);
|
|
150
|
+
expect(top!.compositeScore!).toBeCloseTo(0.9, 2);
|
|
151
|
+
} finally {
|
|
152
|
+
delete process.env.MEMORY_GRAPH_EXPANSION;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -5,7 +5,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
6
|
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
7
7
|
import * as realMemoryModule from "../be/memory";
|
|
8
|
-
import { sourceQuality } from "../be/memory/reranker";
|
|
9
8
|
import { SIMILARITY_THRESHOLD } from "../prompts/memories";
|
|
10
9
|
import type { AgentMemory } from "../types";
|
|
11
10
|
|
|
@@ -111,7 +110,9 @@ mock.module("../be/memory", () => ({
|
|
|
111
110
|
if (options.queryText === "prompt recall") {
|
|
112
111
|
return [
|
|
113
112
|
candidate(memory, 0.95),
|
|
114
|
-
|
|
113
|
+
// Relevance exactly at the threshold; the manual-source boost lifts
|
|
114
|
+
// the composite well above it. The gate must read relevance.
|
|
115
|
+
candidate(thresholdMemory, SIMILARITY_THRESHOLD),
|
|
115
116
|
];
|
|
116
117
|
}
|
|
117
118
|
return [candidate(memory, 0.95)];
|
|
@@ -281,16 +282,25 @@ describe("memory HTTP recall capture gating", () => {
|
|
|
281
282
|
]);
|
|
282
283
|
});
|
|
283
284
|
|
|
284
|
-
test("prompt recall
|
|
285
|
+
test("prompt recall gates on relevance, not the boosted composite score", async () => {
|
|
285
286
|
const response = await callMemoryRoute(
|
|
286
287
|
"POST",
|
|
287
288
|
"/api/memory/search",
|
|
288
289
|
["api", "memory", "search"],
|
|
289
290
|
{ query: "prompt recall", intent: "pre-task memory recall", limit: 5 },
|
|
290
|
-
{ "x-memory-consumption": "prompt" },
|
|
291
|
+
{ "x-memory-consumption": "prompt", "x-source-task-id": sourceTaskId },
|
|
291
292
|
);
|
|
292
293
|
|
|
293
294
|
expect(response.statusCode).toBe(200);
|
|
295
|
+
const threshold = response.body.results.find((r: any) => r.id === thresholdMemoryId);
|
|
296
|
+
expect(threshold.similarity).toBeGreaterThan(SIMILARITY_THRESHOLD);
|
|
297
|
+
expect(threshold.rawSimilarity).toBe(SIMILARITY_THRESHOLD);
|
|
298
|
+
expect(
|
|
299
|
+
await getDbClient().get<{ similarity: number; relevance: number }>(
|
|
300
|
+
"SELECT similarity, relevance FROM memory_retrieval WHERE memoryId = ?",
|
|
301
|
+
[thresholdMemoryId],
|
|
302
|
+
),
|
|
303
|
+
).toEqual({ similarity: threshold.similarity, relevance: SIMILARITY_THRESHOLD });
|
|
294
304
|
expect(response.body.results.map((result: any) => result.accessCount)).toEqual([1, 0]);
|
|
295
305
|
expect(
|
|
296
306
|
await getDbClient().get<{ accessCount: number }>(
|