@desplega.ai/agent-swarm 1.92.0 → 1.92.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/openapi.json +276 -3
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +416 -20
- package/src/be/memory/boot-reembed.ts +85 -0
- package/src/be/memory/constants.ts +44 -2
- package/src/be/memory/providers/openai-embedding.ts +15 -5
- package/src/be/memory/providers/sqlite-store.ts +325 -76
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +43 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +5622 -2543
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +465 -0
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/index.ts +32 -4
- package/src/be/seed-skills/index.ts +0 -7
- package/src/be/skill-sync.ts +91 -7
- package/src/commands/runner.ts +6 -2
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/index.ts +50 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +62 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +2 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/providers/claude-adapter.ts +56 -24
- package/src/script-workflows/workflow-ctx.ts +7 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +2 -0
- package/src/tasks/worker-follow-up.ts +12 -0
- package/src/tests/claude-adapter-binary.test.ts +135 -81
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-e2e.test.ts +6 -6
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-rater-e2e.test.ts +4 -5
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/memory.test.ts +13 -12
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/scripts-mcp-e2e.test.ts +53 -0
- package/src/tests/seed-scripts.test.ts +328 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tests/task-cascade-fail.test.ts +304 -0
- package/src/tools/create-page.ts +2 -2
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +20 -0
- package/src/utils/internal-ai/complete-structured.ts +2 -2
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- package/templates/skills/scheduled-task-resilience/content.md +0 -95
|
@@ -123,13 +123,13 @@ describe("Memory E2E Lifecycle", () => {
|
|
|
123
123
|
|
|
124
124
|
describe("reranking affects result order", () => {
|
|
125
125
|
test("newer memory with same embedding ranks higher", () => {
|
|
126
|
-
//
|
|
126
|
+
// Use task_completion source so recency decay applies (manual has no decay)
|
|
127
127
|
const old = store.store({
|
|
128
128
|
agentId: agentA,
|
|
129
129
|
scope: "agent",
|
|
130
130
|
name: "old knowledge",
|
|
131
131
|
content: "Old deployment docs",
|
|
132
|
-
source: "
|
|
132
|
+
source: "task_completion",
|
|
133
133
|
});
|
|
134
134
|
store.updateEmbedding(old.id, new Float32Array([0.5, 0.5, 0.0]), "test-model");
|
|
135
135
|
|
|
@@ -144,7 +144,7 @@ describe("Memory E2E Lifecycle", () => {
|
|
|
144
144
|
scope: "agent",
|
|
145
145
|
name: "fresh knowledge",
|
|
146
146
|
content: "New deployment docs",
|
|
147
|
-
source: "
|
|
147
|
+
source: "task_completion",
|
|
148
148
|
});
|
|
149
149
|
store.updateEmbedding(fresh.id, new Float32Array([0.5, 0.5, 0.0]), "test-model");
|
|
150
150
|
|
|
@@ -302,7 +302,7 @@ describe("Memory E2E Lifecycle", () => {
|
|
|
302
302
|
content: "Agent A only",
|
|
303
303
|
source: "manual",
|
|
304
304
|
});
|
|
305
|
-
store.updateEmbedding(m1.id, new Float32Array([1, 0, 0]), "test-model");
|
|
305
|
+
store.updateEmbedding(m1.id, new Float32Array([1, 0.3, 0.3]), "test-model");
|
|
306
306
|
agentMemId = m1.id;
|
|
307
307
|
|
|
308
308
|
const m2 = store.store({
|
|
@@ -312,7 +312,7 @@ describe("Memory E2E Lifecycle", () => {
|
|
|
312
312
|
content: "Visible to all",
|
|
313
313
|
source: "manual",
|
|
314
314
|
});
|
|
315
|
-
store.updateEmbedding(m2.id, new Float32Array([0, 1, 0]), "test-model");
|
|
315
|
+
store.updateEmbedding(m2.id, new Float32Array([0.3, 1, 0.3]), "test-model");
|
|
316
316
|
swarmMemId = m2.id;
|
|
317
317
|
|
|
318
318
|
const m3 = store.store({
|
|
@@ -322,7 +322,7 @@ describe("Memory E2E Lifecycle", () => {
|
|
|
322
322
|
content: "Agent B only",
|
|
323
323
|
source: "manual",
|
|
324
324
|
});
|
|
325
|
-
store.updateEmbedding(m3.id, new Float32Array([0, 0, 1]), "test-model");
|
|
325
|
+
store.updateEmbedding(m3.id, new Float32Array([0.3, 0.3, 1]), "test-model");
|
|
326
326
|
otherAgentMemId = m3.id;
|
|
327
327
|
});
|
|
328
328
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { closeDb, initDb } from "../be/db";
|
|
5
|
+
import { EMBEDDING_DIMENSIONS } from "../be/memory/constants";
|
|
6
|
+
import { handleMemory } from "../http/memory";
|
|
7
|
+
import { getPathSegments } from "../http/utils";
|
|
8
|
+
|
|
9
|
+
const TEST_DB_PATH = "./test-memory-health-endpoint.sqlite";
|
|
10
|
+
|
|
11
|
+
function fakeReqRes(path: string) {
|
|
12
|
+
const req = {
|
|
13
|
+
method: "GET",
|
|
14
|
+
url: path,
|
|
15
|
+
headers: {},
|
|
16
|
+
} as unknown as IncomingMessage;
|
|
17
|
+
|
|
18
|
+
const captured = { status: 0, body: "" };
|
|
19
|
+
const res = {
|
|
20
|
+
writeHead(status: number) {
|
|
21
|
+
captured.status = status;
|
|
22
|
+
return this;
|
|
23
|
+
},
|
|
24
|
+
end(chunk?: string) {
|
|
25
|
+
if (chunk) captured.body = chunk;
|
|
26
|
+
return this;
|
|
27
|
+
},
|
|
28
|
+
} as unknown as ServerResponse;
|
|
29
|
+
|
|
30
|
+
return { req, res, captured };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("GET /api/memory/health", () => {
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
36
|
+
try {
|
|
37
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
initDb(TEST_DB_PATH);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
closeDb();
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
46
|
+
try {
|
|
47
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns vector index health JSON", async () => {
|
|
53
|
+
const { req, res, captured } = fakeReqRes("/api/memory/health");
|
|
54
|
+
const handled = await handleMemory(req, res, getPathSegments(req.url || ""), undefined);
|
|
55
|
+
|
|
56
|
+
expect(handled).toBe(true);
|
|
57
|
+
expect(captured.status).toBe(200);
|
|
58
|
+
|
|
59
|
+
const body = JSON.parse(captured.body);
|
|
60
|
+
expect(body).toMatchObject({
|
|
61
|
+
sqliteVec: {
|
|
62
|
+
extensionLoaded: expect.any(Boolean),
|
|
63
|
+
tableExists: expect.any(Boolean),
|
|
64
|
+
initialized: expect.any(Boolean),
|
|
65
|
+
vectorDimensions: EMBEDDING_DIMENSIONS,
|
|
66
|
+
distanceMetric: "cosine",
|
|
67
|
+
},
|
|
68
|
+
counts: {
|
|
69
|
+
total: expect.any(Number),
|
|
70
|
+
withEmbedding: expect.any(Number),
|
|
71
|
+
searchable: expect.any(Number),
|
|
72
|
+
memoryVec: expect.any(Number),
|
|
73
|
+
},
|
|
74
|
+
retrievalMode: expect.any(String),
|
|
75
|
+
reasons: expect.any(Array),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -558,13 +558,12 @@ describe("memory-rater v1.5 — cross-cutting e2e", () => {
|
|
|
558
558
|
);
|
|
559
559
|
|
|
560
560
|
// The usefulness factor at Beta(1,1) is exactly 1.0; a memory with no
|
|
561
|
-
// ratings should score
|
|
562
|
-
//
|
|
561
|
+
// ratings should score = similarity * recency * access * sourceQuality * usefulness.
|
|
562
|
+
// For source=manual: sourceQuality=1.5, recency=1.0 (no decay for manual),
|
|
563
|
+
// access=1.0, usefulness=1.0. So score = 0.5 * 1.5 = 0.75.
|
|
563
564
|
const fresh = buildCandidate(0.5);
|
|
564
565
|
const score = rerank([fresh], { limit: 1 })[0]!.similarity;
|
|
565
|
-
|
|
566
|
-
// usefulness at (1,1) is exactly 1. So score === 0.5 to machine precision.
|
|
567
|
-
expect(score).toBeCloseTo(0.5, 10);
|
|
566
|
+
expect(score).toBeCloseTo(0.75, 10);
|
|
568
567
|
});
|
|
569
568
|
});
|
|
570
569
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
accessBoost,
|
|
4
|
+
computeScore,
|
|
5
|
+
recencyDecay,
|
|
6
|
+
rerank,
|
|
7
|
+
sourceQuality,
|
|
8
|
+
usefulness,
|
|
9
|
+
} from "../be/memory/reranker";
|
|
3
10
|
import type { MemoryCandidate } from "../be/memory/types";
|
|
4
11
|
|
|
5
12
|
function makeCandidate(
|
|
@@ -37,21 +44,33 @@ describe("recencyDecay", () => {
|
|
|
37
44
|
expect(decay).toBeCloseTo(1.0, 5);
|
|
38
45
|
});
|
|
39
46
|
|
|
40
|
-
test("
|
|
47
|
+
test("task_completion at half-life (14d) → ~0.5", () => {
|
|
41
48
|
const created = new Date(now.getTime() - 14 * 86400000).toISOString();
|
|
42
|
-
const decay = recencyDecay(created, now);
|
|
49
|
+
const decay = recencyDecay(created, now, "task_completion");
|
|
43
50
|
expect(decay).toBeCloseTo(0.5, 2);
|
|
44
51
|
});
|
|
45
52
|
|
|
46
|
-
test("
|
|
47
|
-
const created = new Date(now.getTime() -
|
|
48
|
-
const decay = recencyDecay(created, now);
|
|
49
|
-
expect(decay).toBeCloseTo(0.
|
|
53
|
+
test("session_summary at 7d → ~0.5 (7d half-life)", () => {
|
|
54
|
+
const created = new Date(now.getTime() - 7 * 86400000).toISOString();
|
|
55
|
+
const decay = recencyDecay(created, now, "session_summary");
|
|
56
|
+
expect(decay).toBeCloseTo(0.5, 2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("file_index at 180d → ~0.5 (180d half-life)", () => {
|
|
60
|
+
const created = new Date(now.getTime() - 180 * 86400000).toISOString();
|
|
61
|
+
const decay = recencyDecay(created, now, "file_index");
|
|
62
|
+
expect(decay).toBeCloseTo(0.5, 2);
|
|
50
63
|
});
|
|
51
64
|
|
|
52
|
-
test("
|
|
65
|
+
test("manual memory at any age → 1.0 (no decay)", () => {
|
|
53
66
|
const created = new Date(now.getTime() - 365 * 86400000).toISOString();
|
|
54
|
-
const decay = recencyDecay(created, now);
|
|
67
|
+
const decay = recencyDecay(created, now, "manual");
|
|
68
|
+
expect(decay).toBe(1.0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("very old task_completion (365d) → near 0", () => {
|
|
72
|
+
const created = new Date(now.getTime() - 365 * 86400000).toISOString();
|
|
73
|
+
const decay = recencyDecay(created, now, "task_completion");
|
|
55
74
|
expect(decay).toBeLessThan(0.001);
|
|
56
75
|
});
|
|
57
76
|
|
|
@@ -60,6 +79,12 @@ describe("recencyDecay", () => {
|
|
|
60
79
|
const decay = recencyDecay(created, now);
|
|
61
80
|
expect(decay).toBe(1.0);
|
|
62
81
|
});
|
|
82
|
+
|
|
83
|
+
test("no source provided → falls back to task_completion half-life", () => {
|
|
84
|
+
const created = new Date(now.getTime() - 14 * 86400000).toISOString();
|
|
85
|
+
const decay = recencyDecay(created, now);
|
|
86
|
+
expect(decay).toBeCloseTo(0.5, 2);
|
|
87
|
+
});
|
|
63
88
|
});
|
|
64
89
|
|
|
65
90
|
describe("accessBoost", () => {
|
|
@@ -93,31 +118,71 @@ describe("accessBoost", () => {
|
|
|
93
118
|
});
|
|
94
119
|
});
|
|
95
120
|
|
|
121
|
+
describe("sourceQuality", () => {
|
|
122
|
+
test("manual → 1.5", () => {
|
|
123
|
+
expect(sourceQuality("manual")).toBe(1.5);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("file_index → 1.0", () => {
|
|
127
|
+
expect(sourceQuality("file_index")).toBe(1.0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("task_completion → 0.7", () => {
|
|
131
|
+
expect(sourceQuality("task_completion")).toBe(0.7);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("session_summary → 0.5", () => {
|
|
135
|
+
expect(sourceQuality("session_summary")).toBe(0.5);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
96
139
|
describe("computeScore", () => {
|
|
97
140
|
const now = new Date("2026-04-12T12:00:00Z");
|
|
98
141
|
|
|
99
|
-
test("
|
|
142
|
+
test("manual: similarity × 1.0 (no decay) × source(1.5) × boost × usefulness", () => {
|
|
100
143
|
const candidate = makeCandidate({
|
|
101
144
|
similarity: 0.8,
|
|
145
|
+
source: "manual",
|
|
102
146
|
createdAt: now.toISOString(),
|
|
103
147
|
accessedAt: now.toISOString(),
|
|
104
148
|
accessCount: 0,
|
|
105
149
|
});
|
|
106
150
|
const score = computeScore(candidate, now);
|
|
107
|
-
// 0.8 * 1.0 * 1.0 =
|
|
108
|
-
expect(score).toBeCloseTo(
|
|
151
|
+
// 0.8 * 1.0 (no decay for manual) * 1.0 (no boost) * 1.5 (source) * 1.0 (usefulness) = 1.2
|
|
152
|
+
expect(score).toBeCloseTo(1.2, 5);
|
|
109
153
|
});
|
|
110
154
|
|
|
111
|
-
test("
|
|
155
|
+
test("task_completion at 14d → penalized by decay AND source multiplier", () => {
|
|
112
156
|
const candidate = makeCandidate({
|
|
113
157
|
similarity: 0.8,
|
|
158
|
+
source: "task_completion",
|
|
114
159
|
createdAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
115
160
|
accessedAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
116
161
|
accessCount: 0,
|
|
117
162
|
});
|
|
118
163
|
const score = computeScore(candidate, now);
|
|
119
|
-
// 0.8 * 0.5 * 1.0 = 0.
|
|
120
|
-
expect(score).toBeCloseTo(0.
|
|
164
|
+
// 0.8 * 0.5 (14d decay) * 1.0 (no boost) * 0.7 (source) * 1.0 (usefulness) = 0.28
|
|
165
|
+
expect(score).toBeCloseTo(0.28, 2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("old manual vs fresh task_completion: manual wins on relevance", () => {
|
|
169
|
+
const oldManual = makeCandidate({
|
|
170
|
+
similarity: 0.8,
|
|
171
|
+
source: "manual",
|
|
172
|
+
createdAt: new Date(now.getTime() - 76 * 86400000).toISOString(),
|
|
173
|
+
accessedAt: new Date(now.getTime() - 76 * 86400000).toISOString(),
|
|
174
|
+
accessCount: 0,
|
|
175
|
+
});
|
|
176
|
+
const freshTC = makeCandidate({
|
|
177
|
+
similarity: 0.05,
|
|
178
|
+
source: "task_completion",
|
|
179
|
+
createdAt: new Date(now.getTime() - 1 * 86400000).toISOString(),
|
|
180
|
+
accessedAt: new Date(now.getTime() - 1 * 86400000).toISOString(),
|
|
181
|
+
accessCount: 0,
|
|
182
|
+
});
|
|
183
|
+
// This is THE bug we're fixing: with the old flat 14d decay, the old manual
|
|
184
|
+
// memory scored lower than fresh noise. Now manual has no decay.
|
|
185
|
+
expect(computeScore(oldManual, now)).toBeGreaterThan(computeScore(freshTC, now));
|
|
121
186
|
});
|
|
122
187
|
});
|
|
123
188
|
|
|
@@ -166,36 +231,51 @@ describe("rerank", () => {
|
|
|
166
231
|
expect(result[0]!.similarity).toBeGreaterThan(result[1]!.similarity);
|
|
167
232
|
});
|
|
168
233
|
|
|
169
|
-
test("recency boosts newer
|
|
234
|
+
test("recency boosts newer task_completion over older with same raw similarity", () => {
|
|
170
235
|
const candidates = [
|
|
171
236
|
makeCandidate({
|
|
172
237
|
similarity: 0.8,
|
|
173
|
-
|
|
238
|
+
source: "task_completion",
|
|
239
|
+
createdAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
174
240
|
}),
|
|
175
241
|
makeCandidate({
|
|
176
242
|
similarity: 0.8,
|
|
177
|
-
|
|
243
|
+
source: "task_completion",
|
|
244
|
+
createdAt: now.toISOString(),
|
|
178
245
|
}),
|
|
179
246
|
];
|
|
180
247
|
const result = rerank(candidates, { limit: 2, now });
|
|
181
|
-
// Fresh memory should rank higher due to recency decay
|
|
182
248
|
expect(result[0]!.createdAt).toBe(now.toISOString());
|
|
183
249
|
});
|
|
184
250
|
|
|
185
251
|
test("now parameter enables deterministic testing", () => {
|
|
186
252
|
const candidate = makeCandidate({
|
|
187
253
|
similarity: 0.8,
|
|
254
|
+
source: "task_completion",
|
|
188
255
|
createdAt: new Date(now.getTime() - 7 * 86400000).toISOString(),
|
|
189
256
|
});
|
|
190
257
|
const result1 = rerank([candidate], { limit: 1, now });
|
|
191
258
|
const result2 = rerank([candidate], { limit: 1, now });
|
|
192
259
|
expect(result1[0]!.similarity).toBe(result2[0]!.similarity);
|
|
193
260
|
});
|
|
261
|
+
|
|
262
|
+
test("preserves rawSimilarity and compositeScore", () => {
|
|
263
|
+
const candidate = makeCandidate({
|
|
264
|
+
similarity: 0.8,
|
|
265
|
+
source: "manual",
|
|
266
|
+
createdAt: now.toISOString(),
|
|
267
|
+
});
|
|
268
|
+
const result = rerank([candidate], { limit: 1, now });
|
|
269
|
+
expect(result[0]!.rawSimilarity).toBe(0.8);
|
|
270
|
+
expect(result[0]!.compositeScore).toBeDefined();
|
|
271
|
+
// For a fresh manual memory: 0.8 * 1.0 (no decay) * 1.0 (no boost) * 1.5 (source) * 1.0 (usefulness)
|
|
272
|
+
expect(result[0]!.compositeScore).toBeCloseTo(1.2, 5);
|
|
273
|
+
// similarity field = compositeScore
|
|
274
|
+
expect(result[0]!.similarity).toBe(result[0]!.compositeScore);
|
|
275
|
+
});
|
|
194
276
|
});
|
|
195
277
|
|
|
196
278
|
describe("usefulness", () => {
|
|
197
|
-
// The default-floor cases assume MEMORY_DEMOTION_FLOOR is unset/empty.
|
|
198
|
-
// The override case sets and restores the env var.
|
|
199
279
|
let originalFloor: string | undefined;
|
|
200
280
|
beforeEach(() => {
|
|
201
281
|
originalFloor = process.env.MEMORY_DEMOTION_FLOOR;
|
|
@@ -224,10 +304,6 @@ describe("usefulness", () => {
|
|
|
224
304
|
});
|
|
225
305
|
|
|
226
306
|
test("Beta(50,1) → 2 * 50/51 ≈ 1.961 (approaches ceiling, never above 2.0)", () => {
|
|
227
|
-
// NB: the clamp `Math.min(2.0, 2 * mean)` is a defensive ceiling — the
|
|
228
|
-
// formula 2 * α/(α+β) is bounded above by 2 for any finite β > 0, so the
|
|
229
|
-
// clamp only fires on degenerate inputs (β = 0). The plan's "===2.0"
|
|
230
|
-
// expectation was a numerical slip; the asymptote is what we ship.
|
|
231
307
|
expect(usefulness(50, 1)).toBeCloseTo((2 * 50) / 51, 10);
|
|
232
308
|
expect(usefulness(50, 1)).toBeLessThan(2.0);
|
|
233
309
|
});
|
|
@@ -242,110 +318,45 @@ describe("usefulness", () => {
|
|
|
242
318
|
});
|
|
243
319
|
});
|
|
244
320
|
|
|
245
|
-
describe("
|
|
246
|
-
|
|
247
|
-
// MEMORY_DEMOTION_FLOOR=1.0, computeScore must return EXACTLY the same value
|
|
248
|
-
// as a pre-rater build (similarity * recencyDecay * accessBoost).
|
|
249
|
-
const now = new Date("2026-04-12T12:00:00Z");
|
|
250
|
-
|
|
251
|
-
let originalFloor: string | undefined;
|
|
252
|
-
beforeEach(() => {
|
|
253
|
-
originalFloor = process.env.MEMORY_DEMOTION_FLOOR;
|
|
254
|
-
delete process.env.MEMORY_DEMOTION_FLOOR;
|
|
255
|
-
});
|
|
256
|
-
afterEach(() => {
|
|
257
|
-
if (originalFloor === undefined) {
|
|
258
|
-
delete process.env.MEMORY_DEMOTION_FLOOR;
|
|
259
|
-
} else {
|
|
260
|
-
process.env.MEMORY_DEMOTION_FLOOR = originalFloor;
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("computeScore equals similarity * recencyDecay * accessBoost (no usefulness drift)", () => {
|
|
265
|
-
const cases: MemoryCandidate[] = [
|
|
266
|
-
makeCandidate({
|
|
267
|
-
similarity: 0.8,
|
|
268
|
-
createdAt: now.toISOString(),
|
|
269
|
-
accessedAt: now.toISOString(),
|
|
270
|
-
accessCount: 0,
|
|
271
|
-
}),
|
|
272
|
-
makeCandidate({
|
|
273
|
-
similarity: 0.5,
|
|
274
|
-
createdAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
275
|
-
accessedAt: new Date(now.getTime() - 24 * 3600000).toISOString(),
|
|
276
|
-
accessCount: 5,
|
|
277
|
-
}),
|
|
278
|
-
makeCandidate({
|
|
279
|
-
similarity: 0.99,
|
|
280
|
-
createdAt: new Date(now.getTime() - 28 * 86400000).toISOString(),
|
|
281
|
-
accessedAt: new Date(now.getTime() - 72 * 3600000).toISOString(),
|
|
282
|
-
accessCount: 12,
|
|
283
|
-
}),
|
|
284
|
-
];
|
|
285
|
-
|
|
286
|
-
for (const c of cases) {
|
|
287
|
-
const expected =
|
|
288
|
-
c.similarity *
|
|
289
|
-
recencyDecay(c.createdAt, now) *
|
|
290
|
-
accessBoost(c.accessedAt, c.accessCount, now);
|
|
291
|
-
expect(computeScore(c, now)).toBe(expected);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
321
|
+
describe("source-aware scoring: manual memories survive age penalty", () => {
|
|
322
|
+
const now = new Date("2026-06-08T12:00:00Z");
|
|
294
323
|
|
|
295
|
-
test("
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
accessCount: 0,
|
|
316
|
-
}),
|
|
317
|
-
];
|
|
318
|
-
const result = rerank(candidates, { limit: 3, now });
|
|
324
|
+
test("76-day-old manual memory scores higher than 1-day-old noise task_completion", () => {
|
|
325
|
+
// The root-cause scenario from Taras's report: a 76-day-old manual memory
|
|
326
|
+
// with raw similarity 0.8 was being outscored by a 1-day-old noise result
|
|
327
|
+
// with raw similarity 0.05. The old reranker gave the noise result a HIGHER
|
|
328
|
+
// composite score because the flat 14d half-life crushed the old manual
|
|
329
|
+
// memory by 2^(-76/14) = 0.023. Now manual has no decay.
|
|
330
|
+
const oldManual = makeCandidate({
|
|
331
|
+
similarity: 0.8,
|
|
332
|
+
source: "manual",
|
|
333
|
+
createdAt: new Date(now.getTime() - 76 * 86400000).toISOString(),
|
|
334
|
+
accessedAt: new Date(now.getTime() - 76 * 86400000).toISOString(),
|
|
335
|
+
accessCount: 0,
|
|
336
|
+
});
|
|
337
|
+
const freshNoise = makeCandidate({
|
|
338
|
+
similarity: 0.05,
|
|
339
|
+
source: "task_completion",
|
|
340
|
+
createdAt: new Date(now.getTime() - 1 * 86400000).toISOString(),
|
|
341
|
+
accessedAt: new Date(now.getTime() - 1 * 86400000).toISOString(),
|
|
342
|
+
accessCount: 0,
|
|
343
|
+
});
|
|
319
344
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// 0.3 * 2^(-2) = 0.075
|
|
324
|
-
expect(result[0]!.similarity).toBeCloseTo(0.9, 10);
|
|
325
|
-
expect(result[1]!.similarity).toBeCloseTo(0.6 * 2 ** -0.5, 10);
|
|
326
|
-
expect(result[2]!.similarity).toBeCloseTo(0.075, 10);
|
|
345
|
+
const ranked = rerank([freshNoise, oldManual], { limit: 2, now });
|
|
346
|
+
expect(ranked[0]!.source).toBe("manual");
|
|
347
|
+
expect(ranked[0]!.rawSimilarity).toBe(0.8);
|
|
327
348
|
});
|
|
328
349
|
|
|
329
|
-
test("
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
accessedAt: now.toISOString(),
|
|
336
|
-
accessCount: 0,
|
|
337
|
-
alpha: 10,
|
|
338
|
-
beta: 1,
|
|
339
|
-
});
|
|
340
|
-
const baseline = makeCandidate({
|
|
341
|
-
similarity: 0.5,
|
|
342
|
-
createdAt: now.toISOString(),
|
|
343
|
-
accessedAt: now.toISOString(),
|
|
350
|
+
test("session_summary decays fast (7d half-life)", () => {
|
|
351
|
+
const oldSummary = makeCandidate({
|
|
352
|
+
similarity: 0.8,
|
|
353
|
+
source: "session_summary",
|
|
354
|
+
createdAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
355
|
+
accessedAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
344
356
|
accessCount: 0,
|
|
345
357
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
);
|
|
358
|
+
// At 14d with 7d half-life: decay = 2^(-14/7) = 0.25
|
|
359
|
+
// Score: 0.8 * 0.25 * 0.5 (source) = 0.1
|
|
360
|
+
expect(computeScore(oldSummary, now)).toBeCloseTo(0.1, 2);
|
|
350
361
|
});
|
|
351
362
|
});
|