cozo-memory 1.1.2 → 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,8 +28,11 @@ class MemoryServer {
26
28
  mcp;
27
29
  embeddingService;
28
30
  hybridSearch;
31
+ dynamicFusion;
32
+ adaptiveRetrieval;
29
33
  inferenceEngine;
30
34
  initPromise;
35
+ compactionLocks = new Set();
31
36
  // Metrics tracking
32
37
  metrics = {
33
38
  operations: {
@@ -75,6 +80,8 @@ class MemoryServer {
75
80
  console.error(`[DB] Using backend: ${DB_ENGINE}, path: ${fullDbPath}`);
76
81
  this.embeddingService = new embedding_service_1.EmbeddingService();
77
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);
78
85
  this.inferenceEngine = new inference_engine_1.InferenceEngine(this.db, this.embeddingService);
79
86
  this.mcp = new fastmcp_1.FastMCP({
80
87
  name: "cozo-memory-server",
@@ -1422,13 +1429,13 @@ class MemoryServer {
1422
1429
  if (session_id) {
1423
1430
  try {
1424
1431
  await this.db.run(`
1425
- ?[id, last_active, status, metadata] :=
1426
- *session_state{id, metadata},
1427
- id = $id,
1432
+ ?[session_id, last_active, status, metadata] :=
1433
+ *session_state{session_id, metadata},
1434
+ session_id = $session_id,
1428
1435
  last_active = $now,
1429
1436
  status = "active"
1430
- :update session_state {id => last_active, status, metadata}
1431
- `, { id: session_id, now: Math.floor(now / 1000000) });
1437
+ :update session_state {session_id => last_active, status, metadata}
1438
+ `, { session_id: session_id, now: Math.floor(now / 1000000) });
1432
1439
  }
1433
1440
  catch (e) {
1434
1441
  console.warn(`[AddObservation] Session activity update failed for ${session_id}:`, e.message);
@@ -1437,6 +1444,12 @@ class MemoryServer {
1437
1444
  // Optional: Automatic inference after new observation (in background)
1438
1445
  const suggestionsRaw = await this.inferenceEngine.inferRelations(entityId);
1439
1446
  const suggestions = await this.formatInferredRelationsForContext(suggestionsRaw);
1447
+ // Automatic Compaction (Threshold-based)
1448
+ // Skip for compaction-generated observations to prevent recursive loop
1449
+ const metadataKind = args.metadata?.kind;
1450
+ if (metadataKind !== "compaction" && metadataKind !== "session_summary") {
1451
+ this.compactEntity({ entity_id: entityId }).catch(e => console.error(`[AutoCompact] Error for ${entityId}:`, e));
1452
+ }
1440
1453
  const created_at_iso = new Date(Math.floor(now / 1000)).toISOString();
1441
1454
  return {
1442
1455
  id,
@@ -1472,6 +1485,13 @@ class MemoryServer {
1472
1485
  }
1473
1486
  async stopSession(args) {
1474
1487
  await this.initPromise;
1488
+ // Automatic Session Compaction
1489
+ try {
1490
+ await this.compactSession({ session_id: args.id });
1491
+ }
1492
+ catch (e) {
1493
+ console.warn(`[AutoCompact] Session compaction failed for ${args.id}:`, e.message);
1494
+ }
1475
1495
  await this.db.run(`
1476
1496
  ?[session_id, last_active, status, metadata] :=
1477
1497
  *session_state{session_id, last_active, metadata},
@@ -1796,9 +1816,9 @@ ids[id] <- $ids
1796
1816
  else {
1797
1817
  // Select top 5 entities with the most observations
1798
1818
  const res = await this.db.run(`
1799
- ?[id, name, type, count(id)] :=
1800
- *entity{id, name, type, @ "NOW"},
1801
- *observation{entity_id: id, @ "NOW"}
1819
+ ?[eid, ename, etype, count(oid)] :=
1820
+ *entity{id: eid, name: ename, type: etype, @ "NOW"},
1821
+ *observation{entity_id: eid, id: oid, @ "NOW"}
1802
1822
  `);
1803
1823
  entitiesToReflect = res.rows
1804
1824
  .map((r) => ({ id: String(r[0]), name: String(r[1]), type: String(r[2]), count: Number(r[3]) }))
@@ -1859,8 +1879,8 @@ If no special patterns are recognizable, answer with "No new insights".`;
1859
1879
  for (const candidate of candidates) {
1860
1880
  // Check if relationship already exists
1861
1881
  const existing = await this.db.run(`
1862
- ?[count(from_id)] := *relationship{from_id, to_id, relation_type, @ "NOW"},
1863
- from_id = $from, to_id = $to, relation_type = $rel
1882
+ ?[count(fid)] := *relationship{from_id: fid, to_id: tid, relation_type: rel, @ "NOW"},
1883
+ fid = $from, tid = $to, rel = $rel
1864
1884
  `, { from: candidate.from_id, to: candidate.to_id, rel: candidate.relation_type });
1865
1885
  if (Number(existing.rows[0][0]) > 0)
1866
1886
  continue;
@@ -2191,6 +2211,132 @@ ids[id] <- $ids
2191
2211
  return { error: error.message || "Error during ingest" };
2192
2212
  }
2193
2213
  }
2214
+ async compactSession(args) {
2215
+ await this.initPromise;
2216
+ const model = args.model || "demyagent-4b-i1:Q6_K";
2217
+ try {
2218
+ // 1. Get all observations from this session
2219
+ const obsRes = await this.db.run('?[text, ts] := *observation{session_id: $sid, text, created_at, @ "NOW"}, ts = to_int(created_at) :order ts', {
2220
+ sid: args.session_id
2221
+ });
2222
+ if (obsRes.rows.length < 3) {
2223
+ return { status: "skipped", reason: "Too few observations for session compaction" };
2224
+ }
2225
+ const observations = obsRes.rows.map((r) => r[0]);
2226
+ const systemPrompt = `You are a memory compaction module. Summarize the following session observations into exactly 2-3 concise bullet points.
2227
+ Focus only on core facts, preferences, or important events. Skip conversational filler.
2228
+ Format:
2229
+ - Bullet Point 1
2230
+ - Bullet Point 2
2231
+ (- Bullet Point 3)`;
2232
+ const userPrompt = `Session ID: ${args.session_id}\n\nObservations:\n${observations.join("\n")}`;
2233
+ const response = await this.callOllama(model, systemPrompt, userPrompt);
2234
+ if (!response)
2235
+ throw new Error("Ollama summary failed");
2236
+ // 2. Add as preference/long-term info to user profile
2237
+ await this.addObservation({
2238
+ entity_id: exports.USER_ENTITY_ID,
2239
+ text: `Session Summary (${args.session_id}): ${response}`,
2240
+ metadata: { kind: "session_summary", session_id: args.session_id, model },
2241
+ deduplicate: true
2242
+ });
2243
+ return { status: "session_compacted", session_id: args.session_id, summary: response };
2244
+ }
2245
+ catch (error) {
2246
+ console.error(`[CompactSession] Error:`, error);
2247
+ return { error: error.message };
2248
+ }
2249
+ }
2250
+ async compactEntity(args) {
2251
+ if (this.compactionLocks.has(args.entity_id))
2252
+ return { status: "already_compacting" };
2253
+ this.compactionLocks.add(args.entity_id);
2254
+ await this.initPromise;
2255
+ const threshold = args.threshold || 20;
2256
+ const model = args.model || "demyagent-4b-i1:Q6_K";
2257
+ try {
2258
+ // 1. Check count of active observations
2259
+ const countRes = await this.db.run('?[count(oid)] := *observation{entity_id: $eid, id: oid, @ "NOW"}', { eid: args.entity_id });
2260
+ const currentCount = Number(countRes.rows[0][0]);
2261
+ if (currentCount <= threshold) {
2262
+ return { entity_id: args.entity_id, status: "ok", count: currentCount };
2263
+ }
2264
+ console.error(`[CompactEntity] Compacting ${args.entity_id} (Count: ${currentCount})`);
2265
+ // 2. Get older observations (limit to currentCount - threshold/2 to keep some recent context)
2266
+ const toKeep = Math.floor(threshold / 2);
2267
+ const toCompact = currentCount - toKeep;
2268
+ const obsRes = await this.db.run(`
2269
+ ?[oid, txt, ts] := *observation{entity_id: $eid, id: oid, text: txt, created_at, @ "NOW"}, ts = to_int(created_at)
2270
+ :order ts
2271
+ :limit $limit
2272
+ `, { eid: args.entity_id, limit: toCompact });
2273
+ if (obsRes.rows.length === 0)
2274
+ return { entity_id: args.entity_id, status: "no_older_obs_found" };
2275
+ const idsToInvalidate = obsRes.rows.map((r) => String(r[0]));
2276
+ const textsToCompact = obsRes.rows.map((r) => r[1]);
2277
+ // 3. Check for existing ExecutiveSummary
2278
+ const existingSummaryRes = await this.db.run('?[sid, stxt] := *observation{entity_id: $eid, text: stxt, id: sid, @ "NOW"}, regex_matches(stxt, "(?i).*(Executive\\s*Summary|ExecutiveSummary).*") :limit 1', { eid: args.entity_id });
2279
+ let summaryText = "";
2280
+ let oldSummaryId = "";
2281
+ if (existingSummaryRes.rows.length > 0) {
2282
+ oldSummaryId = String(existingSummaryRes.rows[0][0]);
2283
+ summaryText = String(existingSummaryRes.rows[0][1]).replace(/Executive\s*Summary:\s*/i, "").replace(/ExecutiveSummary:\s*/i, "");
2284
+ console.error(`[CompactEntity] Found existing summary to merge: ${oldSummaryId}`);
2285
+ }
2286
+ // 4. Summarize / Merge
2287
+ const systemPrompt = `You are a memory compaction module. You are maintaining an "Executive Summary" for an entity.
2288
+ Merge the NEW observations into the EXISTING summary (if present).
2289
+ The summary should be dense, data-rich, and capture all relevant historical facts.
2290
+ Keep it structured and concise (max 5-8 sentences).
2291
+ Format MUST start with "ExecutiveSummary: " followed by the consolidated content.`;
2292
+ const userPrompt = `Entity ID: ${args.entity_id}\nExisting Summary: ${summaryText || "None"}\n\nNew Info to integrate:\n${textsToCompact.join("\n")}`;
2293
+ console.error(`[CompactEntity] Calling Ollama for ${idsToInvalidate.length} observations...`);
2294
+ const newSummaryText = await this.callOllama(model, systemPrompt, userPrompt);
2295
+ if (!newSummaryText)
2296
+ throw new Error("Ollama compaction failed");
2297
+ // 5. Atomic Update
2298
+ // a) Invalidate old observations
2299
+ for (const oid of idsToInvalidate) {
2300
+ await this.invalidateObservation({ observation_id: oid });
2301
+ }
2302
+ // b) Invalidate old summary if exists
2303
+ if (oldSummaryId) {
2304
+ await this.invalidateObservation({ observation_id: oldSummaryId });
2305
+ }
2306
+ // c) Add new summary
2307
+ await this.addObservation({
2308
+ entity_id: args.entity_id,
2309
+ text: newSummaryText,
2310
+ metadata: { kind: "compaction", model, compacted_count: idsToInvalidate.length }
2311
+ });
2312
+ return { status: "entity_compacted", entity_id: args.entity_id, observations_compacted: idsToInvalidate.length };
2313
+ }
2314
+ catch (error) {
2315
+ console.error(`[CompactEntity] Error for ${args.entity_id}:`, error);
2316
+ return { error: error.message };
2317
+ }
2318
+ finally {
2319
+ this.compactionLocks.delete(args.entity_id);
2320
+ }
2321
+ }
2322
+ async callOllama(model, system, user) {
2323
+ try {
2324
+ const ollamaMod = await import("ollama");
2325
+ const ollamaClient = ollamaMod?.default ?? ollamaMod;
2326
+ const response = await ollamaClient.chat({
2327
+ model,
2328
+ messages: [
2329
+ { role: "system", content: system },
2330
+ { role: "user", content: user },
2331
+ ],
2332
+ });
2333
+ return response?.message?.content?.trim?.() ?? null;
2334
+ }
2335
+ catch (e) {
2336
+ console.error(`[OllamaCall] Error:`, e);
2337
+ return null;
2338
+ }
2339
+ }
2194
2340
  async deleteEntity(args) {
2195
2341
  const startTime = Date.now();
2196
2342
  try {
@@ -2203,9 +2349,9 @@ ids[id] <- $ids
2203
2349
  }
2204
2350
  console.error(`[Delete] Entity found: ${entityRes.rows[0][0]}`);
2205
2351
  // 2. Count related data before deletion
2206
- const obsCount = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
2207
- const relOutCount = await this.db.run('?[count(from_id)] := *relationship{from_id, to_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
2208
- const relInCount = await this.db.run('?[count(to_id)] := *relationship{from_id, to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
2352
+ const obsCount = await this.db.run('?[count(oid)] := *observation{id: oid, entity_id: $id, @ "NOW"}', { id: args.entity_id });
2353
+ const relOutCount = await this.db.run('?[count(fid)] := *relationship{from_id: fid, to_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
2354
+ const relInCount = await this.db.run('?[count(tid)] := *relationship{from_id, to_id: tid, @ "NOW"}, to_id = $id', { id: args.entity_id });
2209
2355
  console.error(`[Delete] Related data: ${obsCount.rows[0][0]} observations, ${relOutCount.rows[0][0]} outgoing relations, ${relInCount.rows[0][0]} incoming relations`);
2210
2356
  // 3. Delete all related data in a transaction (block)
2211
2357
  console.error(`[Delete] Executing deletion transaction...`);
@@ -2243,6 +2389,280 @@ ids[id] <- $ids
2243
2389
  return { error: "Deletion failed", message: error.message };
2244
2390
  }
2245
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
+ }
2601
+ async invalidateObservation(args) {
2602
+ await this.initPromise;
2603
+ const startTime = Date.now();
2604
+ try {
2605
+ const now = Date.now() * 1000;
2606
+ // Invalidate by putting a retraction (assertive=false) at current time
2607
+ const existing = await this.db.run(`?[oid, eid, sid, tid, txt, emb, meta] := *observation{id: oid, entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta, @ "NOW"}, oid = $id`, { id: args.observation_id });
2608
+ if (existing.rows.length === 0) {
2609
+ return { error: "Observation not found or already invalidated" };
2610
+ }
2611
+ const row = existing.rows[0];
2612
+ const params = {
2613
+ id: row[0],
2614
+ v: [now, false],
2615
+ entity_id: row[1],
2616
+ session_id: row[2],
2617
+ task_id: row[3],
2618
+ text: row[4],
2619
+ embedding: row[5],
2620
+ metadata: row[6]
2621
+ };
2622
+ await this.db.run(`
2623
+ ?[id, created_at, entity_id, session_id, task_id, text, embedding, metadata] <- [[$id, $v, $entity_id, $session_id, $task_id, $text, $embedding, $metadata]]
2624
+ :put observation {id, created_at => entity_id, session_id, task_id, text, embedding, metadata}
2625
+ `, params);
2626
+ this.trackOperation('invalidate_observation', startTime);
2627
+ return { status: "Observation invalidated", id: args.observation_id, invalidated_at: now };
2628
+ }
2629
+ catch (error) {
2630
+ console.error("[InvalidateObs] Error:", error);
2631
+ this.trackError('invalidate_observation');
2632
+ return { error: "Invalidation failed", message: error.message };
2633
+ }
2634
+ }
2635
+ async invalidateRelationship(args) {
2636
+ await this.initPromise;
2637
+ const startTime = Date.now();
2638
+ try {
2639
+ const now = Date.now() * 1000;
2640
+ const existing = await this.db.run(`?[f, t, type, strength, metadata] := *relationship{from_id: f, to_id: t, relation_type: type, strength, metadata, @ "NOW"}, f = $f, t = $t, type = $type`, { f: args.from_id, t: args.to_id, type: args.relation_type });
2641
+ if (existing.rows.length === 0) {
2642
+ return { error: "Relationship not found or already invalidated" };
2643
+ }
2644
+ const row = existing.rows[0];
2645
+ const params = {
2646
+ f: row[0],
2647
+ t: row[1],
2648
+ type: row[2],
2649
+ v: [now, false],
2650
+ strength: row[3],
2651
+ metadata: row[4]
2652
+ };
2653
+ await this.db.run(`
2654
+ ?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$f, $t, $type, $v, $strength, $metadata]]
2655
+ :put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
2656
+ `, params);
2657
+ this.trackOperation('invalidate_relationship', startTime);
2658
+ return { status: "Relationship invalidated", from_id: args.from_id, to_id: args.to_id, relation_type: args.relation_type, invalidated_at: now };
2659
+ }
2660
+ catch (error) {
2661
+ console.error("[InvalidateRel] Error:", error);
2662
+ this.trackError('invalidate_relationship');
2663
+ return { error: "Invalidation failed", message: error.message };
2664
+ }
2665
+ }
2246
2666
  async runTransaction(args) {
2247
2667
  await this.initPromise;
2248
2668
  try {
@@ -2444,6 +2864,46 @@ ids[id] <- $ids
2444
2864
  results.push({ action: "delete_entity", id: entity_id });
2445
2865
  break;
2446
2866
  }
2867
+ case "invalidate_observation": {
2868
+ const { observation_id } = params;
2869
+ if (!observation_id) {
2870
+ return { error: `Missing observation_id for invalidate_observation in operation ${i}` };
2871
+ }
2872
+ const now = Date.now() * 1000;
2873
+ allParams[`inv_obs_id${suffix}`] = observation_id;
2874
+ allParams[`inv_obs_v${suffix}`] = [now, false];
2875
+ statements.push(`
2876
+ {
2877
+ ?[oid, cat, eid, sid, tid, txt, emb, meta] :=
2878
+ *observation{id: oid, entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta, @ "NOW"},
2879
+ oid = $inv_obs_id${suffix}, cat = $inv_obs_v${suffix}
2880
+ :put observation {id: oid, created_at: cat => entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta}
2881
+ }
2882
+ `);
2883
+ results.push({ action: "invalidate_observation", id: observation_id });
2884
+ break;
2885
+ }
2886
+ case "invalidate_relation": {
2887
+ const { from_id, to_id, relation_type } = params;
2888
+ if (!from_id || !to_id || !relation_type) {
2889
+ return { error: `Missing from_id, to_id, or relation_type for invalidate_relation in operation ${i}` };
2890
+ }
2891
+ const now = Date.now() * 1000;
2892
+ allParams[`inv_rel_f${suffix}`] = from_id;
2893
+ allParams[`inv_rel_t${suffix}`] = to_id;
2894
+ allParams[`inv_rel_type${suffix}`] = relation_type;
2895
+ allParams[`inv_rel_v${suffix}`] = [now, false];
2896
+ statements.push(`
2897
+ {
2898
+ ?[f, t, type, cat, str, meta] :=
2899
+ *relationship{from_id: f, to_id: t, relation_type: type, strength: str, metadata: meta, @ "NOW"},
2900
+ f = $inv_rel_f${suffix}, t = $inv_rel_t${suffix}, type = $inv_rel_type${suffix}, cat = $inv_rel_v${suffix}
2901
+ :put relationship {from_id: f, to_id: t, relation_type: type, created_at: cat => strength: str, metadata: meta}
2902
+ }
2903
+ `);
2904
+ results.push({ action: "invalidate_relation", from_id, to_id, relation_type });
2905
+ break;
2906
+ }
2447
2907
  default:
2448
2908
  return { error: `Unknown operation: ${op.action}` };
2449
2909
  }
@@ -2623,6 +3083,16 @@ ids[id] <- $ids
2623
3083
  action: zod_1.z.literal("delete_entity"),
2624
3084
  entity_id: zod_1.z.string().describe("ID of the entity to delete"),
2625
3085
  }).passthrough(),
3086
+ zod_1.z.object({
3087
+ action: zod_1.z.literal("invalidate_observation"),
3088
+ observation_id: zod_1.z.string().describe("ID of the observation to invalidate"),
3089
+ }).passthrough(),
3090
+ zod_1.z.object({
3091
+ action: zod_1.z.literal("invalidate_relation"),
3092
+ from_id: zod_1.z.string().describe("Source entity ID"),
3093
+ to_id: zod_1.z.string().describe("Target entity ID"),
3094
+ relation_type: zod_1.z.string().describe("Type of the relationship"),
3095
+ }).passthrough(),
2626
3096
  zod_1.z.object({
2627
3097
  action: zod_1.z.literal("add_observation"),
2628
3098
  entity_id: zod_1.z.string().optional().describe("ID of the entity"),
@@ -2746,7 +3216,7 @@ ids[id] <- $ids
2746
3216
  ]);
2747
3217
  const MutateMemoryParameters = zod_1.z.object({
2748
3218
  action: zod_1.z
2749
- .enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task"])
3219
+ .enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task", "invalidate_observation", "invalidate_relation"])
2750
3220
  .describe("Action (determines which fields are required)"),
2751
3221
  name: zod_1.z.string().optional().describe("For create_entity (required) or add_inference_rule (required)"),
2752
3222
  type: zod_1.z.string().optional().describe("For create_entity (required)"),
@@ -2768,6 +3238,7 @@ ids[id] <- $ids
2768
3238
  relation_type: zod_1.z.string().optional().describe("For create_relation (required)"),
2769
3239
  strength: zod_1.z.number().min(0).max(1).optional().describe("Optional for create_relation"),
2770
3240
  metadata: MetadataSchema.optional().describe("Optional for create_entity/update_entity/add_observation/create_relation/ingest_file"),
3241
+ observation_id: zod_1.z.string().optional().describe("For invalidate_observation (required)"),
2771
3242
  operations: zod_1.z.array(zod_1.z.object({
2772
3243
  action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity"]),
2773
3244
  params: zod_1.z.any().describe("Parameters for the operation as an object")
@@ -2798,6 +3269,8 @@ Supported actions:
2798
3269
  - 'stop_session': Closes a session. Params: { id: string }.
2799
3270
  - 'start_task': Initializes a new task within a session. Params: { name: string, session_id?: string, metadata?: object }.
2800
3271
  - 'stop_task': Marks a task as completed. Params: { id: string }.
3272
+ - 'invalidate_observation': Invalidates (soft-deletes) an observation at the current time. Params: { observation_id: string }.
3273
+ - 'invalidate_relation': Invalidates (soft-deletes) a relationship at the current time. Params: { from_id: string, to_id: string, relation_type: string }.
2801
3274
 
2802
3275
  Validation: Invalid syntax or missing columns in inference rules will result in errors.`,
2803
3276
  parameters: MutateMemoryParameters,
@@ -2825,6 +3298,10 @@ Validation: Invalid syntax or missing columns in inference rules will result in
2825
3298
  return JSON.stringify(await this.createRelation(rest));
2826
3299
  if (action === "run_transaction")
2827
3300
  return JSON.stringify(await this.runTransaction(rest));
3301
+ if (action === "invalidate_observation")
3302
+ return JSON.stringify(await this.invalidateObservation(rest));
3303
+ if (action === "invalidate_relation")
3304
+ return JSON.stringify(await this.invalidateRelationship(rest));
2828
3305
  if (action === "delete_entity")
2829
3306
  return JSON.stringify(await this.deleteEntity({ entity_id: rest.entity_id }));
2830
3307
  if (action === "add_inference_rule")
@@ -2921,13 +3398,56 @@ Validation: Invalid syntax or missing columns in inference rules will result in
2921
3398
  session_id: zod_1.z.string().optional().describe("Prioritize results from this session"),
2922
3399
  task_id: zod_1.z.string().optional().describe("Prioritize results from this task"),
2923
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
+ }),
2924
3444
  ]);
2925
3445
  const QueryMemoryParameters = zod_1.z.object({
2926
3446
  action: zod_1.z
2927
- .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"])
2928
3448
  .describe("Action (determines which fields are required)"),
2929
- query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking/agentic_search"),
2930
- 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"),
2931
3451
  session_id: zod_1.z.string().optional().describe("Optional session ID for context boosting"),
2932
3452
  task_id: zod_1.z.string().optional().describe("Optional task ID for context boosting"),
2933
3453
  filters: zod_1.z.any().optional().describe("Only for advancedSearch"),
@@ -2943,6 +3463,7 @@ Validation: Invalid syntax or missing columns in inference rules will result in
2943
3463
  max_depth: zod_1.z.number().optional().describe("Only for graph_rag/graph_walking: Maximum expansion depth"),
2944
3464
  start_entity_id: zod_1.z.string().optional().describe("Only for graph_walking: Start entity"),
2945
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"),
2946
3467
  });
2947
3468
  this.mcp.addTool({
2948
3469
  name: "query_memory",
@@ -2958,8 +3479,10 @@ Supported actions:
2958
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 }.
2959
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 }.
2960
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.
2961
3484
 
2962
- 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.`,
2963
3486
  parameters: QueryMemoryParameters,
2964
3487
  execute: async (args) => {
2965
3488
  await this.initPromise;
@@ -3130,6 +3653,101 @@ Notes: 'agentic_search' is the most powerful and adaptable, 'context' is ideal f
3130
3653
  });
3131
3654
  return JSON.stringify(results);
3132
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
+ }
3133
3751
  if (input.action === "graph_rag") {
3134
3752
  if (!input.query || input.query.trim().length === 0) {
3135
3753
  return JSON.stringify({ error: "Search query must not be empty." });
@@ -3603,6 +4221,12 @@ Supported actions:
3603
4221
  min_entity_degree: zod_1.z.number().min(0).max(100).optional().default(2),
3604
4222
  model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
3605
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
+ }),
3606
4230
  zod_1.z.object({
3607
4231
  action: zod_1.z.literal("reflect"),
3608
4232
  entity_id: zod_1.z.string().optional().describe("Optional entity ID for targeted reflection"),
@@ -3618,10 +4242,16 @@ Supported actions:
3618
4242
  model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
3619
4243
  min_community_size: zod_1.z.number().min(2).max(100).optional().default(3),
3620
4244
  }),
4245
+ zod_1.z.object({
4246
+ action: zod_1.z.literal("compact"),
4247
+ session_id: zod_1.z.string().optional().describe("Session ID to compact"),
4248
+ entity_id: zod_1.z.string().optional().describe("Entity ID to compact"),
4249
+ model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
4250
+ }),
3621
4251
  ]);
3622
4252
  const ManageSystemParameters = zod_1.z.object({
3623
4253
  action: zod_1.z
3624
- .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory", "summarize_communities"])
4254
+ .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact"])
3625
4255
  .describe("Action (determines which fields are required)"),
3626
4256
  format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
3627
4257
  includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
@@ -3636,10 +4266,12 @@ Supported actions:
3636
4266
  snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
3637
4267
  snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
3638
4268
  metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
3639
- 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"),
3640
4270
  older_than_days: zod_1.z.number().optional().describe("Optional for cleanup"),
3641
4271
  max_observations: zod_1.z.number().optional().describe("Optional for cleanup"),
3642
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)"),
3643
4275
  model: zod_1.z.string().optional().describe("Optional for cleanup/reflect/summarize_communities"),
3644
4276
  entity_id: zod_1.z.string().optional().describe("Optional for reflect"),
3645
4277
  min_community_size: zod_1.z.number().optional().describe("Optional for summarize_communities"),
@@ -3666,6 +4298,9 @@ Supported actions:
3666
4298
  - 'cleanup': Janitor service for consolidation. Params: { confirm: boolean, older_than_days?: number, max_observations?: number, min_entity_degree?: number, model?: string }.
3667
4299
  * With confirm=false: Dry-Run (shows candidates).
3668
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.
3669
4304
  - 'reflect': Reflection service. Analyzes memory for contradictions and insights. Params: { entity_id?: string, model?: string }.
3670
4305
  - 'clear_memory': Resets the entire database. Params: { confirm: boolean (must be true) }.
3671
4306
  - 'summarize_communities': Hierarchical GraphRAG. Generates summaries for entity clusters. Params: { model?: string, min_community_size?: number }.`,
@@ -3764,9 +4399,9 @@ Supported actions:
3764
4399
  if (input.action === "snapshot_create") {
3765
4400
  try {
3766
4401
  // Optimization: Sequential execution and count aggregation instead of full fetch
3767
- const entityResult = await this.db.run('?[count(id)] := *entity{id, @ "NOW"}');
3768
- const obsResult = await this.db.run('?[count(id)] := *observation{id, @ "NOW"}');
3769
- const relResult = await this.db.run('?[count(from_id)] := *relationship{from_id, to_id, @ "NOW"}');
4402
+ const entityResult = await this.db.run('?[count(eid)] := *entity{id: eid, @ "NOW"}');
4403
+ const obsResult = await this.db.run('?[count(oid)] := *observation{id: oid, @ "NOW"}');
4404
+ const relResult = await this.db.run('?[count(fid)] := *relationship{from_id: fid, to_id, @ "NOW"}');
3770
4405
  const snapshot_id = (0, uuid_1.v4)();
3771
4406
  const counts = {
3772
4407
  entities: Number(entityResult.rows[0]?.[0] || 0),
@@ -3856,6 +4491,19 @@ Supported actions:
3856
4491
  return JSON.stringify({ error: error.message || "Error during cleanup" });
3857
4492
  }
3858
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
+ }
3859
4507
  if (input.action === "reflect") {
3860
4508
  try {
3861
4509
  const result = await this.reflectMemory({
@@ -3897,6 +4545,34 @@ Supported actions:
3897
4545
  return JSON.stringify({ error: error.message || "Error clearing memory" });
3898
4546
  }
3899
4547
  }
4548
+ if (input.action === "compact") {
4549
+ try {
4550
+ if (input.session_id) {
4551
+ return JSON.stringify(await this.compactSession({ session_id: input.session_id, model: input.model }));
4552
+ }
4553
+ else if (input.entity_id) {
4554
+ return JSON.stringify(await this.compactEntity({ entity_id: input.entity_id, model: input.model }));
4555
+ }
4556
+ else {
4557
+ // Compact all entities that exceed threshold
4558
+ const res = await this.db.run(`
4559
+ ?[eid, count(oid)] := *observation{entity_id: eid, id: oid, @ "NOW"}
4560
+ `);
4561
+ const threshold = 20;
4562
+ const entitiesToCompact = res.rows
4563
+ .filter((r) => Number(r[1]) > threshold)
4564
+ .map((r) => String(r[0]));
4565
+ const results = [];
4566
+ for (const eid of entitiesToCompact) {
4567
+ results.push(await this.compactEntity({ entity_id: eid, model: input.model }));
4568
+ }
4569
+ return JSON.stringify({ status: "global_compaction_completed", entities_processed: results.length, results });
4570
+ }
4571
+ }
4572
+ catch (error) {
4573
+ return JSON.stringify({ error: error.message || "Error during compaction" });
4574
+ }
4575
+ }
3900
4576
  return JSON.stringify({ error: "Unknown action" });
3901
4577
  },
3902
4578
  });