@desplega.ai/agent-swarm 1.64.0 → 1.65.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.
@@ -1,14 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { chunkContent } from "../be/chunking";
4
- import {
5
- createMemory,
6
- deleteMemoriesBySourcePath,
7
- getDb,
8
- searchMemoriesByVector,
9
- updateMemoryEmbedding,
10
- } from "../be/db";
11
- import { getEmbedding, serializeEmbedding } from "../be/embedding";
4
+ import { getEmbeddingProvider, getMemoryStore } from "../be/memory";
5
+ import { CANDIDATE_SET_MULTIPLIER } from "../be/memory/constants";
6
+ import { rerank } from "../be/memory/reranker";
12
7
  import { AgentMemoryScopeSchema, AgentMemorySourceSchema } from "../types";
13
8
  import { route } from "./route-def";
14
9
  import { json, jsonError } from "./utils";
@@ -54,6 +49,26 @@ const searchMemory = route({
54
49
  },
55
50
  });
56
51
 
52
+ const reEmbedMemory = route({
53
+ method: "post",
54
+ path: "/api/memory/re-embed",
55
+ pattern: ["api", "memory", "re-embed"],
56
+ summary: "Re-embed all memories using the current embedding provider",
57
+ tags: ["Memory"],
58
+ auth: { apiKey: true },
59
+ body: z.object({
60
+ agentId: z
61
+ .string()
62
+ .uuid()
63
+ .optional()
64
+ .describe("Re-embed only this agent's memories. Omit for all."),
65
+ batchSize: z.number().int().min(1).max(100).default(20).describe("Memories per batch"),
66
+ }),
67
+ responses: {
68
+ 202: { description: "Re-embedding started" },
69
+ },
70
+ });
71
+
57
72
  // ─── Handler ─────────────────────────────────────────────────────────────────
58
73
 
59
74
  export async function handleMemory(
@@ -68,10 +83,9 @@ export async function handleMemory(
68
83
 
69
84
  const { agentId, content, name, scope, source, sourceTaskId, sourcePath, tags } = parsed.body;
70
85
 
71
- // Chunk content and create memories in a transaction (with dedup)
86
+ // Chunk content and create memories
72
87
  const contentChunks = chunkContent(content);
73
88
  if (contentChunks.length === 0) {
74
- // Content too small to chunk — create a single memory
75
89
  contentChunks.push({
76
90
  content: content.trim(),
77
91
  chunkIndex: 0,
@@ -80,46 +94,45 @@ export async function handleMemory(
80
94
  });
81
95
  }
82
96
 
83
- const memoryIds = getDb().transaction(() => {
84
- // Delete old chunks if re-indexing same file
85
- if (sourcePath && agentId) {
86
- deleteMemoriesBySourcePath(sourcePath, agentId);
87
- }
97
+ const store = getMemoryStore();
98
+ const provider = getEmbeddingProvider();
88
99
 
89
- const ids: string[] = [];
90
- for (const chunk of contentChunks) {
91
- const memory = createMemory({
92
- agentId: agentId || null,
93
- content: chunk.content,
94
- name,
95
- scope,
96
- source,
97
- sourcePath: sourcePath || null,
98
- sourceTaskId: sourceTaskId || null,
99
- chunkIndex: chunk.chunkIndex,
100
- totalChunks: chunk.totalChunks,
101
- tags: tags || [],
102
- });
103
- ids.push(memory.id);
104
- }
105
- return ids;
106
- })();
100
+ // Dedup delete old chunks for this source path
101
+ if (sourcePath && agentId) {
102
+ store.deleteBySourcePath(sourcePath, agentId);
103
+ }
107
104
 
108
- // Async embeddingfire and forget
105
+ // Atomic batch insert all chunks or none
106
+ const memories = store.storeBatch(
107
+ contentChunks.map((chunk) => ({
108
+ agentId: agentId || null,
109
+ content: chunk.content,
110
+ name,
111
+ scope,
112
+ source,
113
+ sourcePath: sourcePath || null,
114
+ sourceTaskId: sourceTaskId || null,
115
+ chunkIndex: chunk.chunkIndex,
116
+ totalChunks: chunk.totalChunks,
117
+ tags: tags || [],
118
+ })),
119
+ );
120
+
121
+ // Async batch embed (fire and forget)
109
122
  (async () => {
110
- for (let i = 0; i < contentChunks.length; i++) {
111
- try {
112
- const embedding = await getEmbedding(contentChunks[i]!.content);
113
- if (embedding) {
114
- updateMemoryEmbedding(memoryIds[i]!, serializeEmbedding(embedding));
123
+ try {
124
+ const embeddings = await provider.embedBatch(contentChunks.map((c) => c.content));
125
+ for (let i = 0; i < embeddings.length; i++) {
126
+ if (embeddings[i]) {
127
+ store.updateEmbedding(memories[i]!.id, embeddings[i]!, provider.name);
115
128
  }
116
- } catch (err) {
117
- console.error(`[memory] Failed to embed chunk ${memoryIds[i]}:`, (err as Error).message);
118
129
  }
130
+ } catch (err) {
131
+ console.error("[memory] Batch embedding failed:", (err as Error).message);
119
132
  }
120
133
  })();
121
134
 
122
- json(res, { queued: true, memoryIds }, 202);
135
+ json(res, { queued: true, memoryIds: memories.map((m) => m.id) }, 202);
123
136
  return true;
124
137
  }
125
138
 
@@ -135,20 +148,25 @@ export async function handleMemory(
135
148
  const { query, limit } = parsed.body;
136
149
 
137
150
  try {
138
- const queryEmbedding = await getEmbedding(query);
151
+ const provider = getEmbeddingProvider();
152
+ const store = getMemoryStore();
153
+ const queryEmbedding = await provider.embed(query);
154
+
139
155
  if (!queryEmbedding) {
140
156
  json(res, { results: [] });
141
157
  return true;
142
158
  }
143
159
 
144
- const results = searchMemoriesByVector(queryEmbedding, myAgentId, {
160
+ const candidateLimit = Math.min(limit, 20) * CANDIDATE_SET_MULTIPLIER;
161
+ const candidates = store.search(queryEmbedding, myAgentId, {
145
162
  scope: "all",
146
- limit: Math.min(limit, 20),
163
+ limit: candidateLimit,
147
164
  isLead: false,
148
165
  });
166
+ const ranked = rerank(candidates, { limit: Math.min(limit, 20) });
149
167
 
150
168
  json(res, {
151
- results: results.map((r) => ({
169
+ results: ranked.map((r) => ({
152
170
  id: r.id,
153
171
  name: r.name,
154
172
  content: r.content,
@@ -164,5 +182,40 @@ export async function handleMemory(
164
182
  return true;
165
183
  }
166
184
 
185
+ if (reEmbedMemory.match(req.method, pathSegments)) {
186
+ const parsed = await reEmbedMemory.parse(req, res, pathSegments, new URLSearchParams());
187
+ if (!parsed) return true;
188
+
189
+ const { agentId, batchSize } = parsed.body;
190
+ const store = getMemoryStore();
191
+ const provider = getEmbeddingProvider();
192
+ const memories = store.listForReembedding(agentId ? { agentId } : undefined);
193
+
194
+ json(res, { started: true, totalMemories: memories.length }, 202);
195
+
196
+ // Async re-embed in batches
197
+ (async () => {
198
+ for (let i = 0; i < memories.length; i += batchSize) {
199
+ const batch = memories.slice(i, i + batchSize);
200
+ try {
201
+ const embeddings = await provider.embedBatch(batch.map((m) => m.content));
202
+ for (let j = 0; j < embeddings.length; j++) {
203
+ if (embeddings[j]) {
204
+ store.updateEmbedding(batch[j]!.id, embeddings[j]!, provider.name);
205
+ }
206
+ }
207
+ console.log(
208
+ `[memory] Re-embedded batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(memories.length / batchSize)}`,
209
+ );
210
+ } catch (err) {
211
+ console.error("[memory] Re-embed batch failed:", (err as Error).message);
212
+ }
213
+ }
214
+ console.log(`[memory] Re-embedding complete: ${memories.length} memories`);
215
+ })();
216
+
217
+ return true;
218
+ }
219
+
167
220
  return false;
168
221
  }
package/src/server.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  registerMcpServerUpdateTool,
28
28
  } from "./tools/mcp-servers";
29
29
  // Memory capability
30
+ import { registerMemoryDeleteTool } from "./tools/memory-delete";
30
31
  import { registerMemoryGetTool } from "./tools/memory-get";
31
32
  import { registerMemorySearchTool } from "./tools/memory-search";
32
33
  import { registerMyAgentInfoTool } from "./tools/my-agent-info";
@@ -237,6 +238,7 @@ export function createServer() {
237
238
  if (hasCapability("memory")) {
238
239
  registerMemorySearchTool(server);
239
240
  registerMemoryGetTool(server);
241
+ registerMemoryDeleteTool(server);
240
242
  registerInjectLearningTool(server);
241
243
  }
242
244
 
@@ -47,7 +47,8 @@ describe("getAvailablePort", () => {
47
47
  for (let i = 0; i < 10; i++) {
48
48
  ports.add(await getAvailablePort());
49
49
  }
50
- expect(ports.size).toBe(10);
50
+ // Allow minor collisions — OS can recycle ports between bind/release cycles
51
+ expect(ports.size).toBeGreaterThanOrEqual(8);
51
52
  });
52
53
  });
53
54
 
@@ -0,0 +1,453 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, createAgent, getDb, initDb } from "../be/db";
4
+ import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
5
+ import { rerank } from "../be/memory/reranker";
6
+
7
+ const TEST_DB_PATH = "./test-memory-e2e.sqlite";
8
+
9
+ describe("Memory E2E Lifecycle", () => {
10
+ const agentA = "aaaa0000-0000-4000-8000-000000000e01";
11
+ const agentB = "bbbb0000-0000-4000-8000-000000000e02";
12
+ let store: SqliteMemoryStore;
13
+
14
+ beforeAll(async () => {
15
+ for (const suffix of ["", "-wal", "-shm"]) {
16
+ try {
17
+ await unlink(TEST_DB_PATH + suffix);
18
+ } catch {}
19
+ }
20
+ initDb(TEST_DB_PATH);
21
+ createAgent({ id: agentA, name: "E2E Agent A", isLead: false, status: "idle" });
22
+ createAgent({ id: agentB, name: "E2E Agent B", isLead: true, status: "idle" });
23
+ store = new SqliteMemoryStore();
24
+ });
25
+
26
+ afterAll(async () => {
27
+ closeDb();
28
+ for (const suffix of ["", "-wal", "-shm"]) {
29
+ try {
30
+ await unlink(TEST_DB_PATH + suffix);
31
+ } catch {}
32
+ }
33
+ });
34
+
35
+ // ==========================================================================
36
+ // Full lifecycle: store → search → get → delete
37
+ // ==========================================================================
38
+
39
+ describe("store → search → get → delete lifecycle", () => {
40
+ let memoryId: string;
41
+
42
+ test("store creates a memory with correct fields", () => {
43
+ const memory = store.store({
44
+ agentId: agentA,
45
+ scope: "agent",
46
+ name: "deployment info",
47
+ content: "The deployment pipeline uses GitHub Actions with staging on Fly.io",
48
+ source: "manual",
49
+ tags: ["deployment", "ci"],
50
+ });
51
+ memoryId = memory.id;
52
+ expect(memory.id).toBeDefined();
53
+ expect(memory.agentId).toBe(agentA);
54
+ expect(memory.content).toContain("GitHub Actions");
55
+ });
56
+
57
+ test("updateEmbedding stores vector and model name", () => {
58
+ const embedding = new Float32Array([0.8, 0.2, 0.1]);
59
+ store.updateEmbedding(memoryId, embedding, "test-model-v1");
60
+
61
+ // Verify via raw SQL
62
+ const row = getDb()
63
+ .prepare("SELECT embeddingModel FROM agent_memory WHERE id = ?")
64
+ .get(memoryId) as { embeddingModel: string | null };
65
+ expect(row.embeddingModel).toBe("test-model-v1");
66
+ });
67
+
68
+ test("search returns the memory with similarity score", () => {
69
+ const query = new Float32Array([0.8, 0.2, 0.1]); // same as stored
70
+ const results = store.search(query, agentA, { limit: 5 });
71
+ expect(results.length).toBeGreaterThan(0);
72
+
73
+ const found = results.find((r) => r.id === memoryId);
74
+ expect(found).toBeDefined();
75
+ expect(found!.similarity).toBeCloseTo(1.0, 3);
76
+ });
77
+
78
+ test("get retrieves memory and increments accessCount", () => {
79
+ const mem1 = store.get(memoryId);
80
+ expect(mem1).not.toBeNull();
81
+ expect(mem1!.name).toBe("deployment info");
82
+
83
+ // Access count should increment on each get
84
+ const row1 = getDb()
85
+ .prepare("SELECT accessCount FROM agent_memory WHERE id = ?")
86
+ .get(memoryId) as { accessCount: number };
87
+ const count1 = row1.accessCount;
88
+
89
+ store.get(memoryId);
90
+ const row2 = getDb()
91
+ .prepare("SELECT accessCount FROM agent_memory WHERE id = ?")
92
+ .get(memoryId) as { accessCount: number };
93
+ expect(row2.accessCount).toBe(count1 + 1);
94
+ });
95
+
96
+ test("peek reads without incrementing accessCount", () => {
97
+ const rowBefore = getDb()
98
+ .prepare("SELECT accessCount FROM agent_memory WHERE id = ?")
99
+ .get(memoryId) as { accessCount: number };
100
+
101
+ const mem = store.peek(memoryId);
102
+ expect(mem).not.toBeNull();
103
+ expect(mem!.name).toBe("deployment info");
104
+
105
+ const rowAfter = getDb()
106
+ .prepare("SELECT accessCount FROM agent_memory WHERE id = ?")
107
+ .get(memoryId) as { accessCount: number };
108
+ expect(rowAfter.accessCount).toBe(rowBefore.accessCount);
109
+ });
110
+
111
+ test("delete removes the memory", () => {
112
+ const deleted = store.delete(memoryId);
113
+ expect(deleted).toBe(true);
114
+
115
+ const found = store.get(memoryId);
116
+ expect(found).toBeNull();
117
+ });
118
+ });
119
+
120
+ // ==========================================================================
121
+ // Reranking: newer memories rank higher with similar embeddings
122
+ // ==========================================================================
123
+
124
+ describe("reranking affects result order", () => {
125
+ test("newer memory with same embedding ranks higher", () => {
126
+ // Create old memory
127
+ const old = store.store({
128
+ agentId: agentA,
129
+ scope: "agent",
130
+ name: "old knowledge",
131
+ content: "Old deployment docs",
132
+ source: "manual",
133
+ });
134
+ store.updateEmbedding(old.id, new Float32Array([0.5, 0.5, 0.0]), "test-model");
135
+
136
+ // Backdate the old memory's createdAt
137
+ getDb()
138
+ .prepare("UPDATE agent_memory SET createdAt = datetime('now', '-30 days') WHERE id = ?")
139
+ .run(old.id);
140
+
141
+ // Create fresh memory with similar embedding
142
+ const fresh = store.store({
143
+ agentId: agentA,
144
+ scope: "agent",
145
+ name: "fresh knowledge",
146
+ content: "New deployment docs",
147
+ source: "manual",
148
+ });
149
+ store.updateEmbedding(fresh.id, new Float32Array([0.5, 0.5, 0.0]), "test-model");
150
+
151
+ // Search with matching query
152
+ const candidates = store.search(new Float32Array([0.5, 0.5, 0.0]), agentA, {
153
+ limit: 20,
154
+ });
155
+ const ranked = rerank(candidates, { limit: 10 });
156
+
157
+ const freshIdx = ranked.findIndex((r) => r.id === fresh.id);
158
+ const oldIdx = ranked.findIndex((r) => r.id === old.id);
159
+
160
+ expect(freshIdx).toBeGreaterThanOrEqual(0);
161
+ expect(oldIdx).toBeGreaterThanOrEqual(0);
162
+ expect(freshIdx).toBeLessThan(oldIdx); // fresh ranks higher
163
+
164
+ // Cleanup
165
+ store.delete(old.id);
166
+ store.delete(fresh.id);
167
+ });
168
+ });
169
+
170
+ // ==========================================================================
171
+ // TTL expiry filtering
172
+ // ==========================================================================
173
+
174
+ describe("TTL expiry filtering", () => {
175
+ test("task_completion has ~7d TTL", () => {
176
+ const memory = store.store({
177
+ agentId: agentA,
178
+ scope: "agent",
179
+ name: "task result",
180
+ content: "Completed task output",
181
+ source: "task_completion",
182
+ });
183
+
184
+ const row = getDb()
185
+ .prepare("SELECT expiresAt FROM agent_memory WHERE id = ?")
186
+ .get(memory.id) as { expiresAt: string | null };
187
+ expect(row.expiresAt).not.toBeNull();
188
+
189
+ const expiresAt = new Date(row.expiresAt!).getTime();
190
+ const expectedMin = Date.now() + 6 * 24 * 60 * 60 * 1000; // ~6d
191
+ const expectedMax = Date.now() + 8 * 24 * 60 * 60 * 1000; // ~8d
192
+ expect(expiresAt).toBeGreaterThan(expectedMin);
193
+ expect(expiresAt).toBeLessThan(expectedMax);
194
+
195
+ store.delete(memory.id);
196
+ });
197
+
198
+ test("manual source has no TTL (null expiresAt)", () => {
199
+ const memory = store.store({
200
+ agentId: agentA,
201
+ scope: "agent",
202
+ name: "permanent note",
203
+ content: "This should never expire",
204
+ source: "manual",
205
+ });
206
+
207
+ const row = getDb()
208
+ .prepare("SELECT expiresAt FROM agent_memory WHERE id = ?")
209
+ .get(memory.id) as { expiresAt: string | null };
210
+ expect(row.expiresAt).toBeNull();
211
+
212
+ store.delete(memory.id);
213
+ });
214
+
215
+ test("expired memories are excluded from search", () => {
216
+ const memory = store.store({
217
+ agentId: agentA,
218
+ scope: "agent",
219
+ name: "expired item",
220
+ content: "This should be hidden from search",
221
+ source: "session_summary",
222
+ });
223
+ store.updateEmbedding(memory.id, new Float32Array([0.9, 0.1, 0.0]), "test-model");
224
+
225
+ // Force expiry by backdating expiresAt
226
+ getDb()
227
+ .prepare("UPDATE agent_memory SET expiresAt = datetime('now', '-1 day') WHERE id = ?")
228
+ .run(memory.id);
229
+
230
+ // Search should NOT return expired memory
231
+ const results = store.search(new Float32Array([0.9, 0.1, 0.0]), agentA, { limit: 20 });
232
+ const found = results.find((r) => r.id === memory.id);
233
+ expect(found).toBeUndefined();
234
+
235
+ // But get() still returns it (lazy expiry — no hard delete)
236
+ const direct = store.get(memory.id);
237
+ expect(direct).not.toBeNull();
238
+ expect(direct!.name).toBe("expired item");
239
+
240
+ // includeExpired: true should return it in search
241
+ const withExpired = store.search(new Float32Array([0.9, 0.1, 0.0]), agentA, {
242
+ limit: 20,
243
+ includeExpired: true,
244
+ });
245
+ const foundExpired = withExpired.find((r) => r.id === memory.id);
246
+ expect(foundExpired).toBeDefined();
247
+
248
+ store.delete(memory.id);
249
+ });
250
+ });
251
+
252
+ // ==========================================================================
253
+ // Batch operations
254
+ // ==========================================================================
255
+
256
+ describe("storeBatch atomicity", () => {
257
+ test("stores multiple chunks atomically", () => {
258
+ const chunks = [
259
+ { content: "Chunk 0 content", chunkIndex: 0, totalChunks: 3 },
260
+ { content: "Chunk 1 content", chunkIndex: 1, totalChunks: 3 },
261
+ { content: "Chunk 2 content", chunkIndex: 2, totalChunks: 3 },
262
+ ];
263
+
264
+ const memories = store.storeBatch(
265
+ chunks.map((c) => ({
266
+ agentId: agentA,
267
+ scope: "agent" as const,
268
+ name: "batch-test",
269
+ content: c.content,
270
+ source: "file_index" as const,
271
+ sourcePath: "/test/batch.md",
272
+ chunkIndex: c.chunkIndex,
273
+ totalChunks: c.totalChunks,
274
+ })),
275
+ );
276
+
277
+ expect(memories.length).toBe(3);
278
+ for (let i = 0; i < 3; i++) {
279
+ expect(memories[i].chunkIndex).toBe(i);
280
+ expect(memories[i].totalChunks).toBe(3);
281
+ }
282
+
283
+ // Cleanup
284
+ store.deleteBySourcePath("/test/batch.md", agentA);
285
+ });
286
+ });
287
+
288
+ // ==========================================================================
289
+ // Scope visibility
290
+ // ==========================================================================
291
+
292
+ describe("scope visibility rules", () => {
293
+ let agentMemId: string;
294
+ let swarmMemId: string;
295
+ let otherAgentMemId: string;
296
+
297
+ beforeAll(() => {
298
+ const m1 = store.store({
299
+ agentId: agentA,
300
+ scope: "agent",
301
+ name: "A's private",
302
+ content: "Agent A only",
303
+ source: "manual",
304
+ });
305
+ store.updateEmbedding(m1.id, new Float32Array([1, 0, 0]), "test-model");
306
+ agentMemId = m1.id;
307
+
308
+ const m2 = store.store({
309
+ agentId: agentA,
310
+ scope: "swarm",
311
+ name: "shared knowledge",
312
+ content: "Visible to all",
313
+ source: "manual",
314
+ });
315
+ store.updateEmbedding(m2.id, new Float32Array([0, 1, 0]), "test-model");
316
+ swarmMemId = m2.id;
317
+
318
+ const m3 = store.store({
319
+ agentId: agentB,
320
+ scope: "agent",
321
+ name: "B's private",
322
+ content: "Agent B only",
323
+ source: "manual",
324
+ });
325
+ store.updateEmbedding(m3.id, new Float32Array([0, 0, 1]), "test-model");
326
+ otherAgentMemId = m3.id;
327
+ });
328
+
329
+ afterAll(() => {
330
+ store.delete(agentMemId);
331
+ store.delete(swarmMemId);
332
+ store.delete(otherAgentMemId);
333
+ });
334
+
335
+ test("worker sees own agent-scoped + swarm memories", () => {
336
+ const results = store.search(new Float32Array([1, 1, 1]), agentA, { isLead: false });
337
+ const names = results.map((r) => r.name);
338
+ expect(names).toContain("A's private");
339
+ expect(names).toContain("shared knowledge");
340
+ expect(names).not.toContain("B's private");
341
+ });
342
+
343
+ test("lead sees all memories", () => {
344
+ const results = store.search(new Float32Array([1, 1, 1]), agentA, { isLead: true });
345
+ const names = results.map((r) => r.name);
346
+ expect(names).toContain("A's private");
347
+ expect(names).toContain("shared knowledge");
348
+ expect(names).toContain("B's private");
349
+ });
350
+ });
351
+
352
+ // ==========================================================================
353
+ // Stats including expired count
354
+ // ==========================================================================
355
+
356
+ describe("getStats includes expired count", () => {
357
+ test("reports expired memories", () => {
358
+ const mem = store.store({
359
+ agentId: agentA,
360
+ scope: "agent",
361
+ name: "stats-expired",
362
+ content: "Will expire",
363
+ source: "session_summary",
364
+ });
365
+
366
+ // Force expiry
367
+ getDb()
368
+ .prepare("UPDATE agent_memory SET expiresAt = datetime('now', '-1 hour') WHERE id = ?")
369
+ .run(mem.id);
370
+
371
+ const stats = store.getStats(agentA);
372
+ expect(stats.expired).toBeGreaterThanOrEqual(1);
373
+
374
+ store.delete(mem.id);
375
+ });
376
+ });
377
+
378
+ // ==========================================================================
379
+ // Re-embed updates embeddingModel
380
+ // ==========================================================================
381
+
382
+ describe("updateEmbedding tracks model", () => {
383
+ test("re-embedding updates embeddingModel column", () => {
384
+ const mem = store.store({
385
+ agentId: agentA,
386
+ scope: "agent",
387
+ name: "model-track",
388
+ content: "Track model changes",
389
+ source: "manual",
390
+ });
391
+
392
+ // First embed
393
+ store.updateEmbedding(mem.id, new Float32Array([0.1, 0.2, 0.3]), "model-v1");
394
+ let row = getDb()
395
+ .prepare("SELECT embeddingModel FROM agent_memory WHERE id = ?")
396
+ .get(mem.id) as { embeddingModel: string | null };
397
+ expect(row.embeddingModel).toBe("model-v1");
398
+
399
+ // Re-embed with new model
400
+ store.updateEmbedding(mem.id, new Float32Array([0.3, 0.2, 0.1]), "model-v2");
401
+ row = getDb().prepare("SELECT embeddingModel FROM agent_memory WHERE id = ?").get(mem.id) as {
402
+ embeddingModel: string | null;
403
+ };
404
+ expect(row.embeddingModel).toBe("model-v2");
405
+
406
+ store.delete(mem.id);
407
+ });
408
+ });
409
+
410
+ // ==========================================================================
411
+ // listForReembedding
412
+ // ==========================================================================
413
+
414
+ describe("listForReembedding", () => {
415
+ test("returns id and content for all memories", () => {
416
+ const mem = store.store({
417
+ agentId: agentA,
418
+ scope: "agent",
419
+ name: "reembed-list",
420
+ content: "Content for re-embedding",
421
+ source: "manual",
422
+ });
423
+
424
+ const list = store.listForReembedding();
425
+ expect(list.length).toBeGreaterThan(0);
426
+
427
+ const found = list.find((item) => item.id === mem.id);
428
+ expect(found).toBeDefined();
429
+ expect(found!.content).toBe("Content for re-embedding");
430
+
431
+ store.delete(mem.id);
432
+ });
433
+
434
+ test("filters by agentId", () => {
435
+ const mem = store.store({
436
+ agentId: agentB,
437
+ scope: "agent",
438
+ name: "reembed-filtered",
439
+ content: "B's content",
440
+ source: "manual",
441
+ });
442
+
443
+ const listAll = store.listForReembedding();
444
+ const listB = store.listForReembedding({ agentId: agentB });
445
+ expect(listB.length).toBeLessThanOrEqual(listAll.length);
446
+
447
+ const found = listB.find((item) => item.id === mem.id);
448
+ expect(found).toBeDefined();
449
+
450
+ store.delete(mem.id);
451
+ });
452
+ });
453
+ });