@mclawnet/mcp-server 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mclawnet/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -12,11 +12,22 @@
12
12
  "bin": {
13
13
  "clawnet-mcp": "dist/server.js"
14
14
  },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "clean": "rm -rf dist",
24
+ "test": "vitest run"
25
+ },
15
26
  "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.27.1",
17
- "@mclawnet/logger": "0.1.2",
18
- "@mclawnet/skill-manager": "0.1.0",
19
- "@mclawnet/memory": "0.1.1"
27
+ "@mclawnet/logger": "workspace:*",
28
+ "@mclawnet/memory": "workspace:*",
29
+ "@mclawnet/skill-manager": "workspace:*",
30
+ "@modelcontextprotocol/sdk": "^1.27.1"
20
31
  },
21
32
  "devDependencies": {
22
33
  "@types/better-sqlite3": "^7",
@@ -24,10 +35,5 @@
24
35
  "better-sqlite3": "^11.0.0",
25
36
  "typescript": "^5.8.0",
26
37
  "vitest": "^4.0.18"
27
- },
28
- "scripts": {
29
- "build": "tsc",
30
- "clean": "rm -rf dist",
31
- "test": "vitest run"
32
38
  }
33
- }
39
+ }
@@ -1,627 +0,0 @@
1
- /**
2
- * E2E Memory Pipeline Test
3
- *
4
- * Tests the full memory lifecycle as it flows through a swarm session:
5
- * 1. Role stores memories via MCP tools (Pipeline B)
6
- * 2. Swarm retrospective writes collaboration memories + role_stats
7
- * 3. AutoMemorySync exports to auto memory files
8
- * 4. buildMemorySection reads working memories for prompt injection (Pipeline A)
9
- * 5. Distillation extracts experience from conversations
10
- */
11
- import { describe, it, expect, beforeAll, afterAll } from "vitest";
12
- import { mkdtempSync, rmSync, readFileSync, existsSync, unlinkSync } from "node:fs";
13
- import { join } from "node:path";
14
- import { tmpdir } from "node:os";
15
- import type { Database as DatabaseType } from "better-sqlite3";
16
-
17
- import {
18
- buildMemorySection,
19
- distillConversation,
20
- EmbeddingService,
21
- initDatabase,
22
- MemoryStore,
23
- syncToAutoMemory,
24
- } from "@mclawnet/memory";
25
- import {
26
- handleMemoryToolCall,
27
- type MemoryToolContext,
28
- } from "../tools/memory.js";
29
-
30
- // In the original memory package, ToolContext carried roleId. The mcp-server
31
- // API accepts roleId per call. Adapt by wrapping the call site so existing
32
- // test logic stays unchanged.
33
- type ToolContext = MemoryToolContext & { roleId: string };
34
-
35
- function handleToolCall(
36
- name: string,
37
- args: Record<string, unknown>,
38
- ctx: ToolContext,
39
- ) {
40
- const merged = { roleId: ctx.roleId, ...args };
41
- return handleMemoryToolCall(name, merged, { store: ctx.store, embeddingService: ctx.embeddingService });
42
- }
43
-
44
- // ── Shared state ──────────────────────────────────────────────────────
45
-
46
- let db: DatabaseType;
47
- let store: MemoryStore;
48
- let embeddingService: EmbeddingService;
49
- let tmpDir: string;
50
- let dbPath: string;
51
- let memoryDir: string;
52
-
53
- // Contexts for different roles
54
- let devCtx: ToolContext;
55
- let queenCtx: ToolContext;
56
-
57
- // Track stored memory IDs for later assertions
58
- const storedIds: string[] = [];
59
-
60
- beforeAll(() => {
61
- tmpDir = mkdtempSync(join(tmpdir(), "e2e-memory-pipeline-"));
62
- dbPath = join(tmpDir, "test.db");
63
- memoryDir = join(tmpDir, "auto-memory");
64
-
65
- db = initDatabase(dbPath);
66
- store = new MemoryStore(db);
67
- embeddingService = new EmbeddingService(db);
68
-
69
- devCtx = { store, embeddingService, roleId: "role-developer" };
70
- queenCtx = { store, embeddingService, roleId: "role-queen" };
71
- });
72
-
73
- afterAll(() => {
74
- db.close();
75
- if (existsSync(tmpDir)) {
76
- rmSync(tmpDir, { recursive: true });
77
- }
78
- });
79
-
80
- // ── Full lifecycle ────────────────────────────────────────────────────
81
-
82
- describe("Memory Pipeline E2E", () => {
83
- it("full lifecycle: store -> search -> retrospective -> sync -> prompt inject", async () => {
84
- // ── Step 1: Store 3 memories via MCP handleToolCall ──────────────
85
-
86
- const mem1 = await handleToolCall(
87
- "memory_store",
88
- {
89
- content: "使用连接池提升性能3倍",
90
- type: "experience",
91
- domain: "backend",
92
- importance: 0.8,
93
- },
94
- devCtx,
95
- );
96
- const parsed1 = JSON.parse(mem1.content[0].text);
97
- expect(parsed1.success).toBe(true);
98
- storedIds.push(parsed1.id);
99
-
100
- const mem2 = await handleToolCall(
101
- "memory_store",
102
- {
103
- content: "不要在WAL模式下vacuum",
104
- type: "error",
105
- domain: "database",
106
- importance: 0.9,
107
- },
108
- devCtx,
109
- );
110
- const parsed2 = JSON.parse(mem2.content[0].text);
111
- expect(parsed2.success).toBe(true);
112
- storedIds.push(parsed2.id);
113
-
114
- const mem3 = await handleToolCall(
115
- "memory_store",
116
- {
117
- content: "ESM优先于CJS",
118
- type: "preference",
119
- domain: "tooling",
120
- importance: 0.6,
121
- },
122
- devCtx,
123
- );
124
- const parsed3 = JSON.parse(mem3.content[0].text);
125
- expect(parsed3.success).toBe(true);
126
- storedIds.push(parsed3.id);
127
-
128
- // ── Step 2: Search "数据库" finds related memory ─────────────────
129
-
130
- const searchResult = await handleToolCall(
131
- "memory_search",
132
- { query: "数据库性能" },
133
- devCtx,
134
- );
135
- expect(searchResult.isError).toBeUndefined();
136
- const searchParsed = JSON.parse(searchResult.content[0].text);
137
- expect(searchParsed).toBeInstanceOf(Array);
138
- expect(searchParsed.length).toBeGreaterThan(0);
139
-
140
- // ── Step 3: memory_stats returns correct statistics ──────────────
141
-
142
- const statsResult = await handleToolCall("memory_stats", {}, devCtx);
143
- const stats = JSON.parse(statsResult.content[0].text);
144
- expect(stats.totalMemories).toBe(3);
145
- expect(stats.byType.experience).toBe(1);
146
- expect(stats.byType.error).toBe(1);
147
- expect(stats.byType.preference).toBe(1);
148
- expect(stats.topDomains.length).toBeGreaterThanOrEqual(3);
149
-
150
- // ── Step 4: Simulate swarm retrospective (manual persistToDb) ───
151
- // Write collaboration memories as the queen role + populate role_stats
152
-
153
- await store.addMemoryWithEmbedding(
154
- {
155
- roleId: "role-queen",
156
- content: "Developer与Reviewer并行工作效率最高",
157
- type: "pattern",
158
- level: "long-term",
159
- domain: "collaboration",
160
- importance: 0.85,
161
- sourceSwarmIds: ["swarm-test-001"],
162
- },
163
- embeddingService,
164
- );
165
-
166
- await store.addMemoryWithEmbedding(
167
- {
168
- roleId: "role-queen",
169
- content: "分阶段交付比一次性交付风险更低",
170
- type: "experience",
171
- level: "long-term",
172
- domain: "collaboration",
173
- importance: 0.75,
174
- sourceSwarmIds: ["swarm-test-001"],
175
- },
176
- embeddingService,
177
- );
178
-
179
- // Write role_stats directly
180
- db.prepare(
181
- `INSERT OR REPLACE INTO role_stats
182
- (role_id, tasks_completed, tasks_failed, avg_quality_score, avg_rework_count, total_memories, domains, updated_at)
183
- VALUES (@roleId, @completed, @failed, @quality, @rework, @total, @domains, @now)`,
184
- ).run({
185
- roleId: "role-developer",
186
- completed: 5,
187
- failed: 1,
188
- quality: 0.85,
189
- rework: 0.2,
190
- total: 3,
191
- domains: JSON.stringify(["backend", "database", "tooling"]),
192
- now: Date.now(),
193
- });
194
-
195
- // Verify collaboration memories are in DB
196
- const collabMemories = store.getMemoriesByRole("role-queen");
197
- const collabDomainMemories = collabMemories.filter((m) => m.domain === "collaboration");
198
- expect(collabDomainMemories.length).toBeGreaterThanOrEqual(2);
199
-
200
- // Verify role_stats has data
201
- const roleStatsRow = db
202
- .prepare("SELECT * FROM role_stats WHERE role_id = ?")
203
- .get("role-developer") as Record<string, unknown> | undefined;
204
- expect(roleStatsRow).toBeDefined();
205
- expect(roleStatsRow!.tasks_completed).toBe(5);
206
-
207
- // ── Step 5: syncToAutoMemory -> verify output files ──────────────
208
-
209
- const syncResult = syncToAutoMemory(db, {
210
- projectPath: "/test-project",
211
- memoryDir,
212
- });
213
-
214
- expect(syncResult.filesWritten).toContain("MEMORY.md");
215
- expect(syncResult.totalEntries).toBeGreaterThanOrEqual(4);
216
-
217
- // Verify errors.md contains our WAL vacuum memory
218
- expect(syncResult.filesWritten).toContain("errors.md");
219
- const errorsContent = readFileSync(join(memoryDir, "errors.md"), "utf-8");
220
- expect(errorsContent).toContain("不要在WAL模式下vacuum");
221
-
222
- // Verify patterns.md contains our connection pool experience
223
- expect(syncResult.filesWritten).toContain("patterns.md");
224
- const patternsContent = readFileSync(join(memoryDir, "patterns.md"), "utf-8");
225
- expect(patternsContent).toContain("使用连接池提升性能3倍");
226
-
227
- // Verify collaboration.md contains retrospective memories
228
- expect(syncResult.filesWritten).toContain("collaboration.md");
229
- const collabContent = readFileSync(join(memoryDir, "collaboration.md"), "utf-8");
230
- expect(collabContent).toContain("Developer与Reviewer并行工作效率最高");
231
-
232
- // Verify MEMORY.md index has "Top Memories" and topic file links
233
- const indexContent = readFileSync(join(memoryDir, "MEMORY.md"), "utf-8");
234
- expect(indexContent).toContain("Top Memories");
235
- expect(indexContent).toContain("[errors.md](./errors.md)");
236
-
237
- // ── Step 6: Set memory to working level -> buildMemorySection ────
238
-
239
- store.updateMemory(storedIds[0], { level: "working" }); // 连接池 experience
240
- store.updateMemory(storedIds[1], { level: "working" }); // WAL vacuum error
241
-
242
- // Close db before buildMemorySection (it opens its own connection)
243
- db.close();
244
-
245
- const section = buildMemorySection("role-developer", dbPath);
246
- expect(section).toContain("使用连接池提升性能3倍");
247
- expect(section).toContain("不要在WAL模式下vacuum");
248
- // Should contain role-specific instructions
249
- expect(section).toContain("## 开发者专属记忆指南");
250
- // Should NOT have the raw placeholder
251
- expect(section).not.toContain("{workingMemories}");
252
-
253
- // Re-open db for subsequent tests
254
- db = initDatabase(dbPath);
255
- store = new MemoryStore(db);
256
- embeddingService = new EmbeddingService(db);
257
- devCtx = { store, embeddingService, roleId: "role-developer" };
258
- queenCtx = { store, embeddingService, roleId: "role-queen" };
259
- });
260
-
261
- // ── Cross-role search ──────────────────────────────────────────────
262
-
263
- it("cross-role search: developer finds queen collaboration memories", async () => {
264
- // Search as developer with crossRole: true should find queen's collaboration memories
265
- const result = await handleToolCall(
266
- "memory_search",
267
- { query: "协作模式并行工作", crossRole: true },
268
- devCtx,
269
- );
270
-
271
- const parsed = JSON.parse(result.content[0].text);
272
- expect(parsed.length).toBeGreaterThan(0);
273
-
274
- // At least one result should be from queen and about collaboration
275
- const queenResults = parsed.filter(
276
- (m: { roleId: string }) => m.roleId === "role-queen",
277
- );
278
- expect(queenResults.length).toBeGreaterThan(0);
279
- });
280
-
281
- // ── Importance filter ──────────────────────────────────────────────
282
-
283
- it("importance filter: low-importance excluded from sync", async () => {
284
- // Store a low-importance memory
285
- await store.addMemoryWithEmbedding(
286
- {
287
- roleId: "role-developer",
288
- content: "低优先级记忆:临时调试信息",
289
- type: "experience",
290
- domain: "debug",
291
- importance: 0.3,
292
- },
293
- embeddingService,
294
- );
295
-
296
- // Re-sync with default minImportance (0.5)
297
- const secondMemoryDir = join(tmpDir, "auto-memory-filter");
298
- const syncResult = syncToAutoMemory(db, {
299
- projectPath: "/test-project",
300
- memoryDir: secondMemoryDir,
301
- });
302
-
303
- // Low-importance memory should not appear in any output file
304
- for (const filename of syncResult.filesWritten) {
305
- const content = readFileSync(join(secondMemoryDir, filename), "utf-8");
306
- expect(content).not.toContain("低优先级记忆:临时调试信息");
307
- }
308
- });
309
-
310
- // ── Distillation roundtrip ─────────────────────────────────────────
311
-
312
- it("distillation -> search roundtrip", async () => {
313
- const messages = [
314
- { role: "user", content: "这个部署脚本怎么写?" },
315
- {
316
- role: "assistant",
317
- content:
318
- "best practice: 部署脚本应该是幂等的,使用健康检查确认服务正常启动后再切流量",
319
- },
320
- { role: "user", content: "还有什么注意事项?" },
321
- {
322
- role: "assistant",
323
- content: "注意:回滚策略必须提前准备好,不要等出问题才想",
324
- },
325
- ];
326
-
327
- const distillResult = await distillConversation(
328
- messages,
329
- "role-developer",
330
- store,
331
- embeddingService,
332
- );
333
-
334
- expect(distillResult.extracted).toBeGreaterThanOrEqual(1);
335
-
336
- // Now search for the distilled memory via MCP tool
337
- const searchResult = await handleToolCall(
338
- "memory_search",
339
- { query: "部署脚本幂等健康检查" },
340
- devCtx,
341
- );
342
-
343
- const parsed = JSON.parse(searchResult.content[0].text);
344
- expect(parsed.length).toBeGreaterThan(0);
345
- // At least one result should contain content from distillation
346
- const hasDistilled = parsed.some(
347
- (m: { content: string }) =>
348
- m.content.includes("幂等") || m.content.includes("回滚"),
349
- );
350
- expect(hasDistilled).toBe(true);
351
- });
352
-
353
- // ── Idempotent sync ────────────────────────────────────────────────
354
-
355
- it("idempotent sync: repeated sync produces same files", () => {
356
- const idempotentDir = join(tmpDir, "auto-memory-idempotent");
357
-
358
- const result1 = syncToAutoMemory(db, {
359
- projectPath: "/test-project",
360
- memoryDir: idempotentDir,
361
- });
362
- const files1 = result1.filesWritten.map((f) =>
363
- readFileSync(join(idempotentDir, f), "utf-8"),
364
- );
365
-
366
- const result2 = syncToAutoMemory(db, {
367
- projectPath: "/test-project",
368
- memoryDir: idempotentDir,
369
- });
370
- const files2 = result2.filesWritten.map((f) =>
371
- readFileSync(join(idempotentDir, f), "utf-8"),
372
- );
373
-
374
- expect(result1.filesWritten).toEqual(result2.filesWritten);
375
- expect(result1.totalEntries).toEqual(result2.totalEntries);
376
- expect(files1).toEqual(files2);
377
- });
378
- });
379
-
380
- // ── Sprint 3+4 E2E Tests ─────────────────────────────────────────────
381
-
382
- describe("E2E: Fisher Fusion conflict detection", () => {
383
- it("should detect and resolve duplicate content via MCP memory_store", async () => {
384
- // Store a memory first
385
- const result1 = await handleToolCall(
386
- "memory_store",
387
- { content: "Always use TypeScript strict mode", type: "pattern", importance: 0.6 },
388
- devCtx,
389
- );
390
- const parsed1 = JSON.parse(result1.content[0].text);
391
- expect(parsed1.success).toBe(true);
392
- expect(parsed1.fusionAction).toBe("keep_both"); // first store, no conflict
393
-
394
- // Store identical content — should trigger fusion
395
- const result2 = await handleToolCall(
396
- "memory_store",
397
- { content: "Always use TypeScript strict mode", type: "pattern", importance: 0.7 },
398
- devCtx,
399
- );
400
- const parsed2 = JSON.parse(result2.content[0].text);
401
- expect(parsed2.success).toBe(true);
402
- // With hash embeddings, identical text will have cosine=1.0, triggering conflict
403
- // The resolution depends on fisherWeight comparison
404
- expect(["keep_old", "keep_new", "merge"]).toContain(parsed2.fusionAction);
405
- });
406
-
407
- it("should keep_both for different content", async () => {
408
- await handleToolCall(
409
- "memory_store",
410
- { content: "React hooks need dependency arrays", type: "pattern" },
411
- devCtx,
412
- );
413
- const result = await handleToolCall(
414
- "memory_store",
415
- { content: "Database indexes improve query speed", type: "experience" },
416
- devCtx,
417
- );
418
- const parsed = JSON.parse(result.content[0].text);
419
- expect(parsed.fusionAction).toBe("keep_both");
420
- });
421
- });
422
-
423
- describe("E2E: Importance pruning via memory_reflect", () => {
424
- it("should prune working memories exceeding cap via light reflect", async () => {
425
- // Add 55 working-level memories (WORKING_CAP = 50)
426
- for (let i = 0; i < 55; i++) {
427
- store.addMemory({
428
- roleId: "role-developer",
429
- content: `working memory item ${i}`,
430
- type: "experience",
431
- level: "working",
432
- importance: 0.3 + (i % 10) * 0.05,
433
- });
434
- }
435
-
436
- const result = await handleToolCall(
437
- "memory_reflect",
438
- { scope: "light" },
439
- devCtx,
440
- );
441
-
442
- const parsed = JSON.parse(result.content[0].text);
443
- expect(parsed.scope).toBe("light");
444
- expect(parsed.demotedToLongTerm).toBeGreaterThan(0);
445
- // After pruning, working count should be <= 50
446
- const stats = store.getStats("role-developer");
447
- expect(stats.byLevel.working).toBeLessThanOrEqual(50);
448
- });
449
-
450
- it("should recalculate importance and prune all roles via full reflect", async () => {
451
- // Add memories for two different roles
452
- for (let i = 0; i < 10; i++) {
453
- store.addMemory({
454
- roleId: "role-developer",
455
- content: `dev memory ${i}`,
456
- type: "experience",
457
- });
458
- store.addMemory({
459
- roleId: "role-reviewer",
460
- content: `reviewer memory ${i}`,
461
- type: "pattern",
462
- });
463
- }
464
-
465
- const result = await handleToolCall(
466
- "memory_reflect",
467
- { scope: "full" },
468
- devCtx,
469
- );
470
-
471
- const parsed = JSON.parse(result.content[0].text);
472
- expect(parsed.scope).toBe("full");
473
- expect(parsed.recalculated).toBeGreaterThanOrEqual(20);
474
- expect(parsed.rolesProcessed).toBeGreaterThanOrEqual(2);
475
- expect(typeof parsed.profilesRefreshed).toBe("number");
476
- });
477
- });
478
-
479
- describe("E2E: VectorIndex accelerated search", () => {
480
- it("should return correct results using vector index fast path", async () => {
481
- // Store several memories with embeddings via MCP
482
- const topics = [
483
- "React component lifecycle methods",
484
- "Database connection pooling strategies",
485
- "CSS grid layout techniques",
486
- "Node.js stream processing patterns",
487
- "Git branching workflow best practices",
488
- ];
489
-
490
- for (const topic of topics) {
491
- await handleToolCall(
492
- "memory_store",
493
- { content: topic, type: "experience" },
494
- devCtx,
495
- );
496
- }
497
-
498
- // Search should use vectorIndex fast path (index has entries)
499
- const result = await handleToolCall(
500
- "memory_search",
501
- { query: "React component lifecycle methods", limit: 3 },
502
- devCtx,
503
- );
504
-
505
- const parsed = JSON.parse(result.content[0].text);
506
- expect(parsed.length).toBeGreaterThan(0);
507
- // The exact match should be first or near the top
508
- expect(parsed[0].content).toBe("React component lifecycle methods");
509
- });
510
- });
511
-
512
- describe("E2E: Consolidation full pipeline", () => {
513
- it("should run full consolidation with drift check and domain balance", async () => {
514
- // Use a dedicated role so domain percentages are not diluted by prior tests
515
- const consolidationRole = "role-consolidation-test";
516
-
517
- // Store memories with embeddings across domains
518
- for (let i = 0; i < 12; i++) {
519
- await store.addMemoryWithEmbedding(
520
- {
521
- roleId: consolidationRole,
522
- content: `frontend pattern ${i}`,
523
- type: "pattern",
524
- domain: "frontend",
525
- },
526
- embeddingService,
527
- );
528
- }
529
- // Add a few in a different domain
530
- for (let i = 0; i < 3; i++) {
531
- await store.addMemoryWithEmbedding(
532
- {
533
- roleId: consolidationRole,
534
- content: `backend experience ${i}`,
535
- type: "experience",
536
- domain: "backend",
537
- },
538
- embeddingService,
539
- );
540
- }
541
-
542
- // Run consolidation
543
- const { runConsolidation } = await import("@mclawnet/memory");
544
- const result = await runConsolidation(db, {
545
- vectorIndex: store.getVectorIndex(),
546
- domainBalanceWarningThreshold: 0.7,
547
- });
548
-
549
- expect(result.rolesProcessed).toBeGreaterThan(0);
550
- expect(result.recalculated).toBeGreaterThanOrEqual(15);
551
- // 12/15 = 80% frontend on consolidationRole, should trigger domain warning
552
- const frontendWarning = result.domainWarnings.find(
553
- (w) => w.roleId === consolidationRole && w.domain === "frontend",
554
- );
555
- expect(frontendWarning).toBeDefined();
556
- expect(frontendWarning!.percentage).toBeGreaterThan(0.7);
557
- expect(typeof result.profilesRefreshed).toBe("number");
558
- });
559
- });
560
-
561
- describe("E2E: EWC++ drift protection", () => {
562
- it("should protect high-drift memories from pruning during consolidation", async () => {
563
- const { EWCGuard } = await import("@mclawnet/memory");
564
- const { runConsolidation } = await import("@mclawnet/memory");
565
- const { WORKING_CAP } = await import("@mclawnet/memory");
566
-
567
- const roleId = "role-ewc-drift-test";
568
-
569
- // Step 1: Store initial memories with a consistent "direction" and compute profile
570
- for (let i = 0; i < 5; i++) {
571
- await store.addMemoryWithEmbedding(
572
- {
573
- roleId,
574
- content: `core principle ${i} for role stability`,
575
- type: "principle",
576
- importance: 0.9,
577
- },
578
- embeddingService,
579
- );
580
- }
581
-
582
- // Manually set high fisher_weight on these memories so they anchor the profile
583
- const initialMemories = store.getMemoriesByRole(roleId);
584
- for (const mem of initialMemories) {
585
- db.prepare("UPDATE memories SET fisher_weight = 1.0 WHERE id = ?").run(mem.id);
586
- }
587
-
588
- // Compute and save the initial cognitive profile
589
- const ewcGuard = new EWCGuard(db);
590
- const profile = ewcGuard.computeProfile(roleId);
591
- expect(profile).not.toBeNull();
592
- expect(profile!.memoryCount).toBe(5);
593
-
594
- // Step 2: Add many working-level memories to exceed WORKING_CAP and trigger pruning
595
- // These have low importance and different content direction
596
- for (let i = 0; i < WORKING_CAP + 10; i++) {
597
- store.addMemory({
598
- roleId,
599
- content: `disposable working item ${i}`,
600
- type: "experience",
601
- level: "working",
602
- importance: 0.2,
603
- });
604
- }
605
-
606
- // Step 3: Run consolidation — it should check drift and protect core memories
607
- const result = await runConsolidation(db, {
608
- vectorIndex: store.getVectorIndex(),
609
- });
610
-
611
- // Pruning should have demoted some working memories
612
- expect(result.totalDemotedToLongTerm).toBeGreaterThan(0);
613
-
614
- // Step 4: Verify that the original high-fisher-weight principle memories survived
615
- const survivingMemories = store.getMemoriesByRole(roleId);
616
- const principleMemories = survivingMemories.filter(
617
- (m) => m.type === "principle" && m.content.includes("core principle"),
618
- );
619
- // All 5 core principles should still exist (protected by EWC or high importance)
620
- expect(principleMemories.length).toBe(5);
621
-
622
- // Step 5: Verify drift was checked for this role
623
- expect(result.driftResults[roleId]).toBeDefined();
624
- expect(typeof result.driftResults[roleId].driftLoss).toBe("number");
625
- expect(typeof result.driftResults[roleId].isAcceptable).toBe("boolean");
626
- });
627
- });