cozo-memory 1.1.3 → 1.1.4

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/index.js CHANGED
@@ -16,6 +16,8 @@ const fs_1 = __importDefault(require("fs"));
16
16
  const pdf_mjs_1 = require("pdfjs-dist/legacy/build/pdf.mjs");
17
17
  const hybrid_search_1 = require("./hybrid-search");
18
18
  const inference_engine_1 = require("./inference-engine");
19
+ const dynamic_fusion_1 = require("./dynamic-fusion");
20
+ const adaptive_retrieval_1 = require("./adaptive-retrieval");
19
21
  exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
20
22
  const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
21
23
  exports.USER_ENTITY_ID = "global_user_profile";
@@ -26,6 +28,8 @@ class MemoryServer {
26
28
  mcp;
27
29
  embeddingService;
28
30
  hybridSearch;
31
+ dynamicFusion;
32
+ adaptiveRetrieval;
29
33
  inferenceEngine;
30
34
  initPromise;
31
35
  compactionLocks = new Set();
@@ -76,6 +80,8 @@ class MemoryServer {
76
80
  console.error(`[DB] Using backend: ${DB_ENGINE}, path: ${fullDbPath}`);
77
81
  this.embeddingService = new embedding_service_1.EmbeddingService();
78
82
  this.hybridSearch = new hybrid_search_1.HybridSearch(this.db, this.embeddingService);
83
+ this.dynamicFusion = new dynamic_fusion_1.DynamicFusionSearch(this.db, this.embeddingService);
84
+ this.adaptiveRetrieval = new adaptive_retrieval_1.AdaptiveGraphRetrieval(this.db, this.embeddingService);
79
85
  this.inferenceEngine = new inference_engine_1.InferenceEngine(this.db, this.embeddingService);
80
86
  this.mcp = new fastmcp_1.FastMCP({
81
87
  name: "cozo-memory-server",
@@ -2383,6 +2389,215 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
2383
2389
  return { error: "Deletion failed", message: error.message };
2384
2390
  }
2385
2391
  }
2392
+ /**
2393
+ * Memory Defragmentation
2394
+ * Reorganizes memory structure by:
2395
+ * 1. Detecting and merging duplicate/near-duplicate observations using LSH MinHash
2396
+ * 2. Connecting fragmented knowledge islands (small connected components) to main graph
2397
+ * 3. Removing orphaned entities (no observations or relations)
2398
+ *
2399
+ * Inspired by Letta MemFS defragmentation
2400
+ */
2401
+ async defragMemory(args) {
2402
+ await this.initPromise;
2403
+ const startTime = Date.now();
2404
+ try {
2405
+ console.error(`[Defrag] Starting memory defragmentation (confirm=${args.confirm})`);
2406
+ const similarity_threshold = args.similarity_threshold || 0.95; // High threshold for near-duplicates
2407
+ const min_island_size = args.min_island_size || 3; // Islands with <= 3 nodes
2408
+ const stats = {
2409
+ duplicates_found: 0,
2410
+ duplicates_merged: 0,
2411
+ islands_found: 0,
2412
+ islands_connected: 0,
2413
+ orphans_found: 0,
2414
+ orphans_removed: 0,
2415
+ };
2416
+ // Step 1: Find duplicate observations using semantic similarity
2417
+ console.error(`[Defrag] Step 1: Detecting duplicate observations (threshold=${similarity_threshold})`);
2418
+ const allObs = await this.db.run(`
2419
+ ?[id, entity_id, text, embedding] := *observation{id, entity_id, text, embedding, @ "NOW"}
2420
+ `);
2421
+ const duplicatePairs = [];
2422
+ // Compare embeddings for similarity (using existing embeddings)
2423
+ for (let i = 0; i < allObs.rows.length; i++) {
2424
+ for (let j = i + 1; j < allObs.rows.length; j++) {
2425
+ const emb1 = allObs.rows[i][3];
2426
+ const emb2 = allObs.rows[j][3];
2427
+ if (!emb1 || !emb2 || emb1.length === 0 || emb2.length === 0)
2428
+ continue;
2429
+ // Cosine similarity
2430
+ const similarity = this.cosineSimilarity(emb1, emb2);
2431
+ if (similarity >= similarity_threshold) {
2432
+ duplicatePairs.push([
2433
+ String(allObs.rows[i][0]), // id1
2434
+ String(allObs.rows[j][0]), // id2
2435
+ similarity
2436
+ ]);
2437
+ }
2438
+ }
2439
+ }
2440
+ stats.duplicates_found = duplicatePairs.length;
2441
+ console.error(`[Defrag] Found ${stats.duplicates_found} duplicate observation pairs`);
2442
+ // Step 2: Find fragmented knowledge islands (small connected components)
2443
+ console.error(`[Defrag] Step 2: Detecting fragmented knowledge islands`);
2444
+ const components = await this.recomputeConnectedComponents();
2445
+ const smallIslands = Object.entries(components.components || {})
2446
+ .filter(([_, entities]) => entities.length > 0 && entities.length <= min_island_size)
2447
+ .map(([compId, entities]) => ({ compId, entities: entities, size: entities.length }));
2448
+ stats.islands_found = smallIslands.length;
2449
+ console.error(`[Defrag] Found ${stats.islands_found} small knowledge islands (size <= ${min_island_size})`);
2450
+ // Step 3: Find orphaned entities (no observations, no relations)
2451
+ console.error(`[Defrag] Step 3: Detecting orphaned entities`);
2452
+ // Simplified approach: Get all entities, then filter in JavaScript
2453
+ const allEntities = await this.db.run(`
2454
+ ?[id, name, type] := *entity{id, name, type, @ "NOW"}, id != "global_user_profile"
2455
+ `);
2456
+ const orphanedEntities = { rows: [] };
2457
+ for (const row of allEntities.rows) {
2458
+ const entityId = String(row[0]);
2459
+ // Check if entity has observations
2460
+ const obsCheck = await this.db.run(`
2461
+ ?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $eid
2462
+ `, { eid: entityId });
2463
+ const hasObs = Number(obsCheck.rows[0]?.[0] || 0) > 0;
2464
+ if (hasObs)
2465
+ continue;
2466
+ // Check if entity has relationships
2467
+ const relCheck = await this.db.run(`
2468
+ out[count(from_id)] := *relationship{from_id, @ "NOW"}, from_id = $eid
2469
+ in[count(to_id)] := *relationship{to_id, @ "NOW"}, to_id = $eid
2470
+ ?[total] := out[out_count], in[in_count], total = out_count + in_count
2471
+ `, { eid: entityId });
2472
+ const hasRel = Number(relCheck.rows[0]?.[0] || 0) > 0;
2473
+ if (hasRel)
2474
+ continue;
2475
+ // This entity is orphaned
2476
+ orphanedEntities.rows.push(row);
2477
+ }
2478
+ stats.orphans_found = orphanedEntities.rows.length;
2479
+ console.error(`[Defrag] Found ${stats.orphans_found} orphaned entities`);
2480
+ // If not confirmed, return dry-run results
2481
+ if (!args.confirm) {
2482
+ return {
2483
+ status: "dry_run",
2484
+ message: "Defragmentation analysis complete. Set confirm=true to execute.",
2485
+ statistics: stats,
2486
+ preview: {
2487
+ duplicate_samples: duplicatePairs.slice(0, 5).map(([id1, id2, sim]) => ({
2488
+ observation_id_1: id1,
2489
+ observation_id_2: id2,
2490
+ similarity: sim
2491
+ })),
2492
+ island_samples: smallIslands.slice(0, 5),
2493
+ orphan_samples: orphanedEntities.rows.slice(0, 5).map((r) => ({
2494
+ id: String(r[0]),
2495
+ name: String(r[1]),
2496
+ type: String(r[2])
2497
+ }))
2498
+ }
2499
+ };
2500
+ }
2501
+ // Execute defragmentation
2502
+ console.error(`[Defrag] Executing defragmentation...`);
2503
+ // Merge duplicates: Keep first, delete second
2504
+ for (const [id1, id2, similarity] of duplicatePairs) {
2505
+ try {
2506
+ // Delete the duplicate observation
2507
+ await this.db.run(`
2508
+ ?[id, created_at] := *observation{id, created_at}, id = $id2
2509
+ :rm observation {id, created_at}
2510
+ `, { id2 });
2511
+ stats.duplicates_merged++;
2512
+ }
2513
+ catch (e) {
2514
+ console.error(`[Defrag] Failed to merge duplicate ${id2}:`, e.message);
2515
+ }
2516
+ }
2517
+ // Connect islands: Create semantic relations to main graph
2518
+ for (const island of smallIslands) {
2519
+ try {
2520
+ // Find the most central entity in the island
2521
+ const islandEntityIds = island.entities.map((e) => e.id);
2522
+ // Find semantically similar entities in the main graph (not in this island)
2523
+ for (const entityId of islandEntityIds) {
2524
+ const entityData = await this.db.run(`
2525
+ ?[embedding] := *entity{id: $id, embedding, @ "NOW"}
2526
+ `, { id: entityId });
2527
+ if (entityData.rows.length === 0)
2528
+ continue;
2529
+ const embedding = entityData.rows[0][0];
2530
+ if (!embedding || embedding.length === 0)
2531
+ continue;
2532
+ // Find similar entity in main graph
2533
+ const similarEntities = await this.db.run(`
2534
+ ~entity:semantic { id: target_id | query: $emb, k: 1, bind_distance: dist }
2535
+ ?[target_id, dist] := ~entity:semantic { id: target_id | query: $emb, k: 1, bind_distance: dist },
2536
+ target_id != $id
2537
+ `, { emb: embedding, id: entityId });
2538
+ if (similarEntities.rows.length > 0) {
2539
+ const targetId = String(similarEntities.rows[0][0]);
2540
+ const distance = Number(similarEntities.rows[0][1]);
2541
+ const similarity = 1 - distance;
2542
+ // Create bridge relation
2543
+ if (similarity >= 0.7) {
2544
+ await this.createRelation({
2545
+ from_id: entityId,
2546
+ to_id: targetId,
2547
+ relation_type: "semantically_related",
2548
+ strength: similarity,
2549
+ metadata: { created_by: "defrag", reason: "island_connection" }
2550
+ });
2551
+ stats.islands_connected++;
2552
+ break; // One connection per island is enough
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+ catch (e) {
2558
+ console.error(`[Defrag] Failed to connect island ${island.compId}:`, e.message);
2559
+ }
2560
+ }
2561
+ // Remove orphaned entities
2562
+ for (const row of orphanedEntities.rows) {
2563
+ try {
2564
+ const entityId = String(row[0]);
2565
+ await this.deleteEntity({ entity_id: entityId });
2566
+ stats.orphans_removed++;
2567
+ }
2568
+ catch (e) {
2569
+ console.error(`[Defrag] Failed to remove orphan ${row[0]}:`, e.message);
2570
+ }
2571
+ }
2572
+ this.trackOperation('defrag', startTime);
2573
+ return {
2574
+ status: "completed",
2575
+ message: `Defragmentation complete: ${stats.duplicates_merged} duplicates merged, ${stats.islands_connected} islands connected, ${stats.orphans_removed} orphans removed`,
2576
+ statistics: stats,
2577
+ duration_ms: Date.now() - startTime
2578
+ };
2579
+ }
2580
+ catch (error) {
2581
+ console.error("[Defrag] Error during defragmentation:", error);
2582
+ this.trackError('defrag');
2583
+ return { error: "Defragmentation failed", message: error.message };
2584
+ }
2585
+ }
2586
+ cosineSimilarity(a, b) {
2587
+ if (a.length !== b.length)
2588
+ return 0;
2589
+ let dotProduct = 0;
2590
+ let normA = 0;
2591
+ let normB = 0;
2592
+ for (let i = 0; i < a.length; i++) {
2593
+ dotProduct += a[i] * b[i];
2594
+ normA += a[i] * a[i];
2595
+ normB += b[i] * b[i];
2596
+ }
2597
+ if (normA === 0 || normB === 0)
2598
+ return 0;
2599
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
2600
+ }
2386
2601
  async invalidateObservation(args) {
2387
2602
  await this.initPromise;
2388
2603
  const startTime = Date.now();
@@ -3183,13 +3398,56 @@ Validation: Invalid syntax or missing columns in inference rules will result in
3183
3398
  session_id: zod_1.z.string().optional().describe("Prioritize results from this session"),
3184
3399
  task_id: zod_1.z.string().optional().describe("Prioritize results from this task"),
3185
3400
  }),
3401
+ zod_1.z.object({
3402
+ action: zod_1.z.literal("dynamic_fusion"),
3403
+ query: zod_1.z.string().describe("Search query"),
3404
+ config: zod_1.z.object({
3405
+ vector: zod_1.z.object({
3406
+ enabled: zod_1.z.boolean().optional().default(true),
3407
+ weight: zod_1.z.number().min(0).max(1).optional().default(0.4),
3408
+ topK: zod_1.z.number().optional().default(20),
3409
+ efSearch: zod_1.z.number().optional().default(100),
3410
+ }).optional(),
3411
+ sparse: zod_1.z.object({
3412
+ enabled: zod_1.z.boolean().optional().default(true),
3413
+ weight: zod_1.z.number().min(0).max(1).optional().default(0.3),
3414
+ topK: zod_1.z.number().optional().default(20),
3415
+ minScore: zod_1.z.number().optional().default(0.1),
3416
+ }).optional(),
3417
+ fts: zod_1.z.object({
3418
+ enabled: zod_1.z.boolean().optional().default(true),
3419
+ weight: zod_1.z.number().min(0).max(1).optional().default(0.2),
3420
+ topK: zod_1.z.number().optional().default(20),
3421
+ fuzzy: zod_1.z.boolean().optional().default(true),
3422
+ }).optional(),
3423
+ graph: zod_1.z.object({
3424
+ enabled: zod_1.z.boolean().optional().default(true),
3425
+ weight: zod_1.z.number().min(0).max(1).optional().default(0.1),
3426
+ maxDepth: zod_1.z.number().min(1).max(3).optional().default(2),
3427
+ maxResults: zod_1.z.number().optional().default(20),
3428
+ relationTypes: zod_1.z.array(zod_1.z.string()).optional(),
3429
+ }).optional(),
3430
+ fusion: zod_1.z.object({
3431
+ strategy: zod_1.z.enum(['rrf', 'weighted_sum', 'max', 'adaptive']).optional().default('rrf'),
3432
+ rrfK: zod_1.z.number().optional().default(60),
3433
+ minScore: zod_1.z.number().optional().default(0.0),
3434
+ deduplication: zod_1.z.boolean().optional().default(true),
3435
+ }).optional(),
3436
+ }).optional().describe("Dynamic fusion configuration (all paths optional)"),
3437
+ limit: zod_1.z.number().optional().default(10).describe("Maximum number of results to return"),
3438
+ }),
3439
+ zod_1.z.object({
3440
+ action: zod_1.z.literal("adaptive_retrieval"),
3441
+ query: zod_1.z.string().describe("Search query for adaptive retrieval"),
3442
+ limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
3443
+ }),
3186
3444
  ]);
3187
3445
  const QueryMemoryParameters = zod_1.z.object({
3188
3446
  action: zod_1.z
3189
- .enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search"])
3447
+ .enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search", "dynamic_fusion", "adaptive_retrieval"])
3190
3448
  .describe("Action (determines which fields are required)"),
3191
- query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking/agentic_search"),
3192
- limit: zod_1.z.number().optional().describe("Only for search/advancedSearch/graph_rag/graph_walking"),
3449
+ query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking/agentic_search/dynamic_fusion/adaptive_retrieval"),
3450
+ limit: zod_1.z.number().optional().describe("Only for search/advancedSearch/graph_rag/graph_walking/dynamic_fusion/adaptive_retrieval"),
3193
3451
  session_id: zod_1.z.string().optional().describe("Optional session ID for context boosting"),
3194
3452
  task_id: zod_1.z.string().optional().describe("Optional task ID for context boosting"),
3195
3453
  filters: zod_1.z.any().optional().describe("Only for advancedSearch"),
@@ -3205,6 +3463,7 @@ Validation: Invalid syntax or missing columns in inference rules will result in
3205
3463
  max_depth: zod_1.z.number().optional().describe("Only for graph_rag/graph_walking: Maximum expansion depth"),
3206
3464
  start_entity_id: zod_1.z.string().optional().describe("Only for graph_walking: Start entity"),
3207
3465
  rerank: zod_1.z.boolean().optional().describe("Only for search/advancedSearch/agentic_search: Enable Cross-Encoder reranking"),
3466
+ config: zod_1.z.any().optional().describe("Only for dynamic_fusion: Fusion configuration object"),
3208
3467
  });
3209
3468
  this.mcp.addTool({
3210
3469
  name: "query_memory",
@@ -3220,8 +3479,10 @@ Supported actions:
3220
3479
  - 'graph_rag': Graph-based reasoning (Hybrid RAG). Finds semantic vector seeds first, then expands via graph traversals. Ideal for multi-hop reasoning. Params: { query: string, max_depth?: number, limit?: number }.
3221
3480
  - 'graph_walking': Recursive semantic graph search. Starts at vector seeds or an entity and follows relationships to other semantically relevant entities. Params: { query: string, start_entity_id?: string, max_depth?: number, limit?: number }.
3222
3481
  - 'agentic_search': Auto-Routing Search. Uses local LLM to analyze intent and routes the query automatically to the best strategy (Vector, Graph, or Community Summaries). Params: { query: string, limit?: number }.
3482
+ - 'adaptive_retrieval': GraphRAG-R1 inspired adaptive retrieval with Progressive Retrieval Attenuation (PRA) and Cost-Aware F1 (CAF) scoring. Automatically selects optimal strategy based on query complexity and historical performance. Params: { query: string, limit?: number }.
3483
+ - 'dynamic_fusion': Advanced multi-path fusion search combining Vector (HNSW), Sparse (keyword), FTS (full-text), and Graph traversal with configurable weights and strategies. Params: { query: string, config?: { vector?, sparse?, fts?, graph?, fusion? }, limit?: number }. Each path can be enabled/disabled and weighted independently. Fusion strategies: 'rrf' (Reciprocal Rank Fusion), 'weighted_sum', 'max', 'adaptive'. Returns results with path contribution details and performance stats.
3223
3484
 
3224
- Notes: 'agentic_search' is the most powerful and adaptable, 'context' is ideal for exploratory questions. 'search' and 'advancedSearch' are better for targeted fact retrieval.`,
3485
+ Notes: 'adaptive_retrieval' learns from usage and optimizes over time. 'dynamic_fusion' provides the most control and transparency over retrieval paths. 'agentic_search' is the most adaptive. 'context' is ideal for exploratory questions. 'search' and 'advancedSearch' are better for targeted fact retrieval.`,
3225
3486
  parameters: QueryMemoryParameters,
3226
3487
  execute: async (args) => {
3227
3488
  await this.initPromise;
@@ -3392,6 +3653,101 @@ Notes: 'agentic_search' is the most powerful and adaptable, 'context' is ideal f
3392
3653
  });
3393
3654
  return JSON.stringify(results);
3394
3655
  }
3656
+ if (input.action === "dynamic_fusion") {
3657
+ const startTime = Date.now();
3658
+ if (!input.query || input.query.trim().length === 0) {
3659
+ return JSON.stringify({ error: "Search query must not be empty." });
3660
+ }
3661
+ try {
3662
+ console.log('[query_memory] Dynamic Fusion search:', {
3663
+ query: input.query,
3664
+ config: input.config ? 'custom' : 'default',
3665
+ limit: input.limit
3666
+ });
3667
+ // Execute dynamic fusion search
3668
+ const { results, stats } = await this.dynamicFusion.search(input.query, input.config || {});
3669
+ // Apply limit
3670
+ const limitedResults = results.slice(0, input.limit || 10);
3671
+ const response = {
3672
+ results: limitedResults,
3673
+ stats: {
3674
+ ...stats,
3675
+ totalTime: Date.now() - startTime,
3676
+ resultsReturned: limitedResults.length,
3677
+ resultsTotal: results.length
3678
+ },
3679
+ metadata: {
3680
+ query: input.query,
3681
+ fusionStrategy: input.config?.fusion?.strategy || 'rrf',
3682
+ enabledPaths: {
3683
+ vector: input.config?.vector?.enabled !== false,
3684
+ sparse: input.config?.sparse?.enabled !== false,
3685
+ fts: input.config?.fts?.enabled !== false,
3686
+ graph: input.config?.graph?.enabled !== false
3687
+ }
3688
+ }
3689
+ };
3690
+ console.log('[query_memory] Dynamic Fusion completed:', {
3691
+ resultsReturned: limitedResults.length,
3692
+ totalTime: response.stats.totalTime,
3693
+ pathContributions: stats.pathContributions
3694
+ });
3695
+ return JSON.stringify(response);
3696
+ }
3697
+ catch (error) {
3698
+ console.error('[query_memory] Dynamic Fusion error:', error);
3699
+ return JSON.stringify({
3700
+ error: "Dynamic fusion search failed",
3701
+ details: error.message
3702
+ });
3703
+ }
3704
+ }
3705
+ if (input.action === "adaptive_retrieval") {
3706
+ const startTime = Date.now();
3707
+ if (!input.query || input.query.trim().length === 0) {
3708
+ return JSON.stringify({ error: "Search query must not be empty." });
3709
+ }
3710
+ try {
3711
+ console.log('[query_memory] Adaptive Retrieval search:', {
3712
+ query: input.query,
3713
+ limit: input.limit
3714
+ });
3715
+ // Execute adaptive retrieval
3716
+ const result = await this.adaptiveRetrieval.retrieve(input.query, input.limit || 10);
3717
+ const response = {
3718
+ results: result.results,
3719
+ metadata: {
3720
+ query: input.query,
3721
+ strategy: result.strategy,
3722
+ retrievalCount: result.retrievalCount,
3723
+ latency: result.latency,
3724
+ cafScore: result.cafScore,
3725
+ totalTime: Date.now() - startTime
3726
+ },
3727
+ performance: {
3728
+ strategyUsed: result.strategy,
3729
+ retrievalCalls: result.retrievalCount,
3730
+ costAwareF1: result.cafScore,
3731
+ latencyMs: result.latency
3732
+ }
3733
+ };
3734
+ console.log('[query_memory] Adaptive Retrieval completed:', {
3735
+ strategy: result.strategy,
3736
+ resultsReturned: result.results.length,
3737
+ retrievalCount: result.retrievalCount,
3738
+ cafScore: result.cafScore?.toFixed(3),
3739
+ totalTime: response.metadata.totalTime
3740
+ });
3741
+ return JSON.stringify(response);
3742
+ }
3743
+ catch (error) {
3744
+ console.error('[query_memory] Adaptive Retrieval error:', error);
3745
+ return JSON.stringify({
3746
+ error: "Adaptive retrieval search failed",
3747
+ details: error.message
3748
+ });
3749
+ }
3750
+ }
3395
3751
  if (input.action === "graph_rag") {
3396
3752
  if (!input.query || input.query.trim().length === 0) {
3397
3753
  return JSON.stringify({ error: "Search query must not be empty." });
@@ -3865,6 +4221,12 @@ Supported actions:
3865
4221
  min_entity_degree: zod_1.z.number().min(0).max(100).optional().default(2),
3866
4222
  model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
3867
4223
  }),
4224
+ zod_1.z.object({
4225
+ action: zod_1.z.literal("defrag"),
4226
+ confirm: zod_1.z.boolean().describe("Must be true to confirm defragmentation"),
4227
+ similarity_threshold: zod_1.z.number().min(0.8).max(1.0).optional().default(0.95).describe("Similarity threshold for duplicate detection (0.8-1.0)"),
4228
+ min_island_size: zod_1.z.number().min(1).max(10).optional().default(3).describe("Maximum size of knowledge islands to connect"),
4229
+ }),
3868
4230
  zod_1.z.object({
3869
4231
  action: zod_1.z.literal("reflect"),
3870
4232
  entity_id: zod_1.z.string().optional().describe("Optional entity ID for targeted reflection"),
@@ -3889,7 +4251,7 @@ Supported actions:
3889
4251
  ]);
3890
4252
  const ManageSystemParameters = zod_1.z.object({
3891
4253
  action: zod_1.z
3892
- .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory", "summarize_communities", "compact"])
4254
+ .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact"])
3893
4255
  .describe("Action (determines which fields are required)"),
3894
4256
  format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
3895
4257
  includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
@@ -3904,10 +4266,12 @@ Supported actions:
3904
4266
  snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
3905
4267
  snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
3906
4268
  metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
3907
- confirm: zod_1.z.boolean().optional().describe("Required for cleanup/clear_memory and must be true"),
4269
+ confirm: zod_1.z.boolean().optional().describe("Required for cleanup/defrag/clear_memory and must be true"),
3908
4270
  older_than_days: zod_1.z.number().optional().describe("Optional for cleanup"),
3909
4271
  max_observations: zod_1.z.number().optional().describe("Optional for cleanup"),
3910
4272
  min_entity_degree: zod_1.z.number().optional().describe("Optional for cleanup"),
4273
+ similarity_threshold: zod_1.z.number().optional().describe("Optional for defrag (0.8-1.0, default 0.95)"),
4274
+ min_island_size: zod_1.z.number().optional().describe("Optional for defrag (1-10, default 3)"),
3911
4275
  model: zod_1.z.string().optional().describe("Optional for cleanup/reflect/summarize_communities"),
3912
4276
  entity_id: zod_1.z.string().optional().describe("Optional for reflect"),
3913
4277
  min_community_size: zod_1.z.number().optional().describe("Optional for summarize_communities"),
@@ -3934,6 +4298,9 @@ Supported actions:
3934
4298
  - 'cleanup': Janitor service for consolidation. Params: { confirm: boolean, older_than_days?: number, max_observations?: number, min_entity_degree?: number, model?: string }.
3935
4299
  * With confirm=false: Dry-Run (shows candidates).
3936
4300
  * With confirm=true: Merges old/isolated fragments using LLM (Executive Summary) and removes noise.
4301
+ - 'defrag': Memory defragmentation. Reorganizes memory structure by detecting/merging duplicates, connecting fragmented knowledge islands, and removing orphaned entities. Params: { confirm: boolean, similarity_threshold?: number (0.8-1.0, default 0.95), min_island_size?: number (1-10, default 3) }.
4302
+ * With confirm=false: Dry-Run (shows analysis and candidates).
4303
+ * With confirm=true: Executes defragmentation and returns statistics.
3937
4304
  - 'reflect': Reflection service. Analyzes memory for contradictions and insights. Params: { entity_id?: string, model?: string }.
3938
4305
  - 'clear_memory': Resets the entire database. Params: { confirm: boolean (must be true) }.
3939
4306
  - 'summarize_communities': Hierarchical GraphRAG. Generates summaries for entity clusters. Params: { model?: string, min_community_size?: number }.`,
@@ -4124,6 +4491,19 @@ Supported actions:
4124
4491
  return JSON.stringify({ error: error.message || "Error during cleanup" });
4125
4492
  }
4126
4493
  }
4494
+ if (input.action === "defrag") {
4495
+ try {
4496
+ const result = await this.defragMemory({
4497
+ confirm: Boolean(input.confirm),
4498
+ similarity_threshold: input.similarity_threshold,
4499
+ min_island_size: input.min_island_size,
4500
+ });
4501
+ return JSON.stringify(result);
4502
+ }
4503
+ catch (error) {
4504
+ return JSON.stringify({ error: error.message || "Error during defragmentation" });
4505
+ }
4506
+ }
4127
4507
  if (input.action === "reflect") {
4128
4508
  try {
4129
4509
  const result = await this.reflectMemory({