@hasna/mementos 0.4.41 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cli/index.js +2797 -1611
  2. package/dist/db/database.d.ts.map +1 -1
  3. package/dist/db/entities.d.ts.map +1 -1
  4. package/dist/db/memories.d.ts +1 -0
  5. package/dist/db/memories.d.ts.map +1 -1
  6. package/dist/db/relations.d.ts.map +1 -1
  7. package/dist/db/webhook_hooks.d.ts +25 -0
  8. package/dist/db/webhook_hooks.d.ts.map +1 -0
  9. package/dist/index.d.ts +6 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2187 -1331
  12. package/dist/lib/auto-memory-queue.d.ts +46 -0
  13. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  14. package/dist/lib/auto-memory.d.ts +18 -0
  15. package/dist/lib/auto-memory.d.ts.map +1 -0
  16. package/dist/lib/built-in-hooks.d.ts +12 -0
  17. package/dist/lib/built-in-hooks.d.ts.map +1 -0
  18. package/dist/lib/dedup.d.ts +33 -0
  19. package/dist/lib/dedup.d.ts.map +1 -0
  20. package/dist/lib/focus.d.ts +58 -0
  21. package/dist/lib/focus.d.ts.map +1 -0
  22. package/dist/lib/hooks.d.ts +50 -0
  23. package/dist/lib/hooks.d.ts.map +1 -0
  24. package/dist/lib/providers/anthropic.d.ts +21 -0
  25. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  26. package/dist/lib/providers/base.d.ts +96 -0
  27. package/dist/lib/providers/base.d.ts.map +1 -0
  28. package/dist/lib/providers/cerebras.d.ts +20 -0
  29. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  30. package/dist/lib/providers/grok.d.ts +19 -0
  31. package/dist/lib/providers/grok.d.ts.map +1 -0
  32. package/dist/lib/providers/index.d.ts +7 -0
  33. package/dist/lib/providers/index.d.ts.map +1 -0
  34. package/dist/lib/providers/openai-compat.d.ts +18 -0
  35. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  36. package/dist/lib/providers/openai.d.ts +20 -0
  37. package/dist/lib/providers/openai.d.ts.map +1 -0
  38. package/dist/lib/providers/registry.d.ts +38 -0
  39. package/dist/lib/providers/registry.d.ts.map +1 -0
  40. package/dist/lib/search.d.ts.map +1 -1
  41. package/dist/mcp/index.js +6851 -5544
  42. package/dist/server/index.d.ts.map +1 -1
  43. package/dist/server/index.js +2716 -1596
  44. package/dist/types/hooks.d.ts +136 -0
  45. package/dist/types/hooks.d.ts.map +1 -0
  46. package/dist/types/index.d.ts +7 -0
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -341,6 +341,30 @@ var MIGRATIONS = [
341
341
  CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
342
342
  CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
343
343
  INSERT OR IGNORE INTO _migrations (id) VALUES (8);
344
+ `,
345
+ `
346
+ ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
347
+ CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
348
+ INSERT OR IGNORE INTO _migrations (id) VALUES (9);
349
+ `,
350
+ `
351
+ CREATE TABLE IF NOT EXISTS webhook_hooks (
352
+ id TEXT PRIMARY KEY,
353
+ type TEXT NOT NULL,
354
+ handler_url TEXT NOT NULL,
355
+ priority INTEGER NOT NULL DEFAULT 50,
356
+ blocking INTEGER NOT NULL DEFAULT 0,
357
+ agent_id TEXT,
358
+ project_id TEXT,
359
+ description TEXT,
360
+ enabled INTEGER NOT NULL DEFAULT 1,
361
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
362
+ invocation_count INTEGER NOT NULL DEFAULT 0,
363
+ failure_count INTEGER NOT NULL DEFAULT 0
364
+ );
365
+ CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
366
+ CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
367
+ INSERT OR IGNORE INTO _migrations (id) VALUES (10);
344
368
  `
345
369
  ];
346
370
  var _db = null;
@@ -438,1387 +462,1150 @@ function containsSecrets(text) {
438
462
  return false;
439
463
  }
440
464
 
441
- // src/db/agents.ts
442
- var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
443
- function parseAgentRow(row) {
465
+ // src/lib/hooks.ts
466
+ var _idCounter = 0;
467
+ function generateHookId() {
468
+ return `hook_${++_idCounter}_${Date.now().toString(36)}`;
469
+ }
470
+
471
+ class HookRegistry {
472
+ hooks = new Map;
473
+ register(reg) {
474
+ const id = generateHookId();
475
+ const hook = {
476
+ ...reg,
477
+ id,
478
+ priority: reg.priority ?? 50
479
+ };
480
+ this.hooks.set(id, hook);
481
+ return id;
482
+ }
483
+ unregister(hookId) {
484
+ const hook = this.hooks.get(hookId);
485
+ if (!hook)
486
+ return false;
487
+ if (hook.builtin)
488
+ return false;
489
+ this.hooks.delete(hookId);
490
+ return true;
491
+ }
492
+ list(type) {
493
+ const all = [...this.hooks.values()];
494
+ if (!type)
495
+ return all;
496
+ return all.filter((h) => h.type === type);
497
+ }
498
+ async runHooks(type, context) {
499
+ const matching = this.getMatchingHooks(type, context);
500
+ if (matching.length === 0)
501
+ return true;
502
+ matching.sort((a, b) => a.priority - b.priority);
503
+ for (const hook of matching) {
504
+ if (hook.blocking) {
505
+ try {
506
+ const result = await hook.handler(context);
507
+ if (result === false)
508
+ return false;
509
+ } catch (err) {
510
+ console.error(`[hooks] blocking hook ${hook.id} (${type}) threw:`, err);
511
+ }
512
+ } else {
513
+ Promise.resolve().then(() => hook.handler(context)).catch((err) => console.error(`[hooks] non-blocking hook ${hook.id} (${type}) threw:`, err));
514
+ }
515
+ }
516
+ return true;
517
+ }
518
+ getMatchingHooks(type, context) {
519
+ const ctx = context;
520
+ return [...this.hooks.values()].filter((hook) => {
521
+ if (hook.type !== type)
522
+ return false;
523
+ if (hook.agentId && hook.agentId !== ctx.agentId)
524
+ return false;
525
+ if (hook.projectId && hook.projectId !== ctx.projectId)
526
+ return false;
527
+ return true;
528
+ });
529
+ }
530
+ stats() {
531
+ const all = [...this.hooks.values()];
532
+ const byType = {};
533
+ for (const hook of all) {
534
+ byType[hook.type] = (byType[hook.type] ?? 0) + 1;
535
+ }
536
+ return {
537
+ total: all.length,
538
+ byType,
539
+ blocking: all.filter((h) => h.blocking).length,
540
+ nonBlocking: all.filter((h) => !h.blocking).length
541
+ };
542
+ }
543
+ }
544
+ var hookRegistry = new HookRegistry;
545
+
546
+ // src/db/entity-memories.ts
547
+ function parseEntityRow(row) {
444
548
  return {
445
549
  id: row["id"],
446
550
  name: row["name"],
447
- session_id: row["session_id"] || null,
551
+ type: row["type"],
448
552
  description: row["description"] || null,
449
- role: row["role"] || null,
450
553
  metadata: JSON.parse(row["metadata"] || "{}"),
451
- active_project_id: row["active_project_id"] || null,
554
+ project_id: row["project_id"] || null,
452
555
  created_at: row["created_at"],
453
- last_seen_at: row["last_seen_at"]
556
+ updated_at: row["updated_at"]
454
557
  };
455
558
  }
456
- function registerAgent(name, sessionId, description, role, projectId, db) {
457
- const d = db || getDatabase();
458
- const timestamp = now();
459
- const normalizedName = name.trim().toLowerCase();
460
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
461
- if (existing) {
462
- const existingId = existing["id"];
463
- const existingSessionId = existing["session_id"] || null;
464
- const existingLastSeen = existing["last_seen_at"];
465
- if (sessionId && existingSessionId && existingSessionId !== sessionId) {
466
- const lastSeenMs = new Date(existingLastSeen).getTime();
467
- const nowMs = Date.now();
468
- if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
469
- throw new AgentConflictError({
470
- existing_id: existingId,
471
- existing_name: normalizedName,
472
- last_seen_at: existingLastSeen,
473
- session_hint: existingSessionId.slice(0, 8),
474
- working_dir: null
475
- });
476
- }
477
- }
478
- d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
479
- timestamp,
480
- sessionId ?? existingSessionId,
481
- existingId
482
- ]);
483
- if (description) {
484
- d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
485
- }
486
- if (role) {
487
- d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
488
- }
489
- if (projectId !== undefined) {
490
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
491
- }
492
- return getAgent(existingId, d);
493
- }
494
- const id = shortUuid();
495
- d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
496
- return getAgent(id, d);
559
+ function parseEntityMemoryRow(row) {
560
+ return {
561
+ entity_id: row["entity_id"],
562
+ memory_id: row["memory_id"],
563
+ role: row["role"],
564
+ created_at: row["created_at"]
565
+ };
497
566
  }
498
- function getAgent(idOrName, db) {
567
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
499
568
  const d = db || getDatabase();
500
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
501
- if (row)
502
- return parseAgentRow(row);
503
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
504
- if (row)
505
- return parseAgentRow(row);
506
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
507
- if (rows.length === 1)
508
- return parseAgentRow(rows[0]);
509
- return null;
569
+ const timestamp = now();
570
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
571
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
572
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
573
+ return parseEntityMemoryRow(row);
510
574
  }
511
- function listAgents(db) {
575
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
512
576
  const d = db || getDatabase();
513
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
514
- return rows.map(parseAgentRow);
577
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
515
578
  }
516
- function touchAgent(idOrName, db) {
579
+ function getMemoriesForEntity(entityId, db) {
517
580
  const d = db || getDatabase();
518
- const agent = getAgent(idOrName, d);
519
- if (!agent)
520
- return;
521
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
581
+ const rows = d.query(`SELECT m.* FROM memories m
582
+ INNER JOIN entity_memories em ON em.memory_id = m.id
583
+ WHERE em.entity_id = ?
584
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
585
+ return rows.map(parseMemoryRow);
522
586
  }
523
- function listAgentsByProject(projectId, db) {
587
+ function getEntitiesForMemory(memoryId, db) {
524
588
  const d = db || getDatabase();
525
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
526
- return rows.map(parseAgentRow);
589
+ const rows = d.query(`SELECT e.* FROM entities e
590
+ INNER JOIN entity_memories em ON em.entity_id = e.id
591
+ WHERE em.memory_id = ?
592
+ ORDER BY e.name ASC`).all(memoryId);
593
+ return rows.map(parseEntityRow);
527
594
  }
528
- function updateAgent(id, updates, db) {
595
+ function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
529
596
  const d = db || getDatabase();
530
- const agent = getAgent(id, d);
531
- if (!agent)
532
- return null;
533
597
  const timestamp = now();
534
- if (updates.name) {
535
- const normalizedNewName = updates.name.trim().toLowerCase();
536
- if (normalizedNewName !== agent.name) {
537
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
538
- if (existing) {
539
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
540
- }
541
- d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
598
+ const tx = d.transaction(() => {
599
+ const stmt = d.prepare(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
600
+ VALUES (?, ?, ?, ?)`);
601
+ for (const entityId of entityIds) {
602
+ stmt.run(entityId, memoryId, role, timestamp);
542
603
  }
604
+ });
605
+ tx();
606
+ }
607
+ function getEntityMemoryLinks(entityId, memoryId, db) {
608
+ const d = db || getDatabase();
609
+ const conditions = [];
610
+ const params = [];
611
+ if (entityId) {
612
+ conditions.push("entity_id = ?");
613
+ params.push(entityId);
543
614
  }
544
- if (updates.description !== undefined) {
545
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
546
- }
547
- if (updates.role !== undefined) {
548
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
549
- }
550
- if (updates.metadata !== undefined) {
551
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
615
+ if (memoryId) {
616
+ conditions.push("memory_id = ?");
617
+ params.push(memoryId);
552
618
  }
553
- if ("active_project_id" in updates) {
554
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
619
+ let sql = "SELECT * FROM entity_memories";
620
+ if (conditions.length > 0) {
621
+ sql += ` WHERE ${conditions.join(" AND ")}`;
555
622
  }
556
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
557
- return getAgent(agent.id, d);
623
+ sql += " ORDER BY created_at DESC";
624
+ const rows = d.query(sql).all(...params);
625
+ return rows.map(parseEntityMemoryRow);
558
626
  }
559
627
 
560
- // src/db/projects.ts
561
- function parseProjectRow(row) {
628
+ // src/db/memories.ts
629
+ function runEntityExtraction(_memory, _projectId, _d) {}
630
+ function parseMemoryRow(row) {
562
631
  return {
563
632
  id: row["id"],
564
- name: row["name"],
565
- path: row["path"],
566
- description: row["description"] || null,
567
- memory_prefix: row["memory_prefix"] || null,
633
+ key: row["key"],
634
+ value: row["value"],
635
+ category: row["category"],
636
+ scope: row["scope"],
637
+ summary: row["summary"] || null,
638
+ tags: JSON.parse(row["tags"] || "[]"),
639
+ importance: row["importance"],
640
+ source: row["source"],
641
+ status: row["status"],
642
+ pinned: !!row["pinned"],
643
+ agent_id: row["agent_id"] || null,
644
+ project_id: row["project_id"] || null,
645
+ session_id: row["session_id"] || null,
646
+ metadata: JSON.parse(row["metadata"] || "{}"),
647
+ access_count: row["access_count"],
648
+ version: row["version"],
649
+ expires_at: row["expires_at"] || null,
568
650
  created_at: row["created_at"],
569
- updated_at: row["updated_at"]
651
+ updated_at: row["updated_at"],
652
+ accessed_at: row["accessed_at"] || null
570
653
  };
571
654
  }
572
- function registerProject(name, path, description, memoryPrefix, db) {
655
+ function createMemory(input, dedupeMode = "merge", db) {
573
656
  const d = db || getDatabase();
574
657
  const timestamp = now();
575
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
576
- if (existing) {
577
- const existingId = existing["id"];
578
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
579
- timestamp,
580
- existingId
581
- ]);
582
- return parseProjectRow(existing);
658
+ let expiresAt = input.expires_at || null;
659
+ if (input.ttl_ms && !expiresAt) {
660
+ expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
583
661
  }
584
662
  const id = uuid();
585
- d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
586
- return getProject(id, d);
587
- }
588
- function getProject(idOrPath, db) {
589
- const d = db || getDatabase();
590
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
591
- if (row)
592
- return parseProjectRow(row);
593
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
594
- if (row)
595
- return parseProjectRow(row);
596
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
597
- if (row)
598
- return parseProjectRow(row);
599
- return null;
600
- }
601
- function listProjects(db) {
602
- const d = db || getDatabase();
603
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
604
- return rows.map(parseProjectRow);
605
- }
606
-
607
- // src/lib/extractor.ts
608
- var TECH_KEYWORDS = new Set([
609
- "typescript",
610
- "javascript",
611
- "python",
612
- "rust",
613
- "go",
614
- "java",
615
- "ruby",
616
- "swift",
617
- "kotlin",
618
- "react",
619
- "vue",
620
- "angular",
621
- "svelte",
622
- "nextjs",
623
- "bun",
624
- "node",
625
- "deno",
626
- "sqlite",
627
- "postgres",
628
- "mysql",
629
- "redis",
630
- "docker",
631
- "kubernetes",
632
- "git",
633
- "npm",
634
- "yarn",
635
- "pnpm",
636
- "webpack",
637
- "vite",
638
- "tailwind",
639
- "prisma",
640
- "drizzle",
641
- "zod",
642
- "commander",
643
- "express",
644
- "fastify",
645
- "hono"
646
- ]);
647
- var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
648
- var URL_RE = /https?:\/\/[^\s)]+/g;
649
- var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
650
- var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
651
- function getSearchText(memory) {
652
- const parts = [memory.key, memory.value];
653
- if (memory.summary)
654
- parts.push(memory.summary);
655
- return parts.join(" ");
656
- }
657
- function extractEntities(memory, db) {
658
- const text = getSearchText(memory);
659
- const entityMap = new Map;
660
- function add(name, type, confidence) {
661
- const normalized = name.toLowerCase();
662
- if (normalized.length < 3)
663
- return;
664
- const existing = entityMap.get(normalized);
665
- if (!existing || existing.confidence < confidence) {
666
- entityMap.set(normalized, { name: normalized, type, confidence });
667
- }
668
- }
669
- for (const match of text.matchAll(FILE_PATH_RE)) {
670
- add(match[1].trim(), "file", 0.9);
671
- }
672
- for (const match of text.matchAll(URL_RE)) {
673
- add(match[0], "api", 0.8);
674
- }
675
- for (const match of text.matchAll(NPM_PACKAGE_RE)) {
676
- add(match[0], "tool", 0.85);
677
- }
678
- try {
679
- const d = db || getDatabase();
680
- const agents = listAgents(d);
681
- const textLower2 = text.toLowerCase();
682
- for (const agent of agents) {
683
- const nameLower = agent.name.toLowerCase();
684
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
685
- add(agent.name, "person", 0.95);
686
- }
687
- }
688
- } catch {}
689
- try {
690
- const d = db || getDatabase();
691
- const projects = listProjects(d);
692
- const textLower2 = text.toLowerCase();
693
- for (const project of projects) {
694
- const nameLower = project.name.toLowerCase();
695
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
696
- add(project.name, "project", 0.95);
697
- }
698
- }
699
- } catch {}
700
- const textLower = text.toLowerCase();
701
- for (const keyword of TECH_KEYWORDS) {
702
- const re = new RegExp(`\\b${keyword}\\b`, "i");
703
- if (re.test(textLower)) {
704
- add(keyword, "tool", 0.7);
705
- }
706
- }
707
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
708
- add(match[1], "concept", 0.5);
709
- }
710
- return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
711
- }
712
-
713
- // src/db/entities.ts
714
- function parseEntityRow(row) {
715
- return {
716
- id: row["id"],
717
- name: row["name"],
718
- type: row["type"],
719
- description: row["description"] || null,
720
- metadata: JSON.parse(row["metadata"] || "{}"),
721
- project_id: row["project_id"] || null,
722
- created_at: row["created_at"],
723
- updated_at: row["updated_at"]
724
- };
725
- }
726
- function createEntity(input, db) {
727
- const d = db || getDatabase();
728
- const timestamp = now();
663
+ const tags = input.tags || [];
664
+ const tagsJson = JSON.stringify(tags);
729
665
  const metadataJson = JSON.stringify(input.metadata || {});
730
- const existing = d.query(`SELECT * FROM entities
731
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
732
- if (existing) {
733
- const sets = ["updated_at = ?"];
734
- const params = [timestamp];
735
- if (input.description !== undefined) {
736
- sets.push("description = ?");
737
- params.push(input.description);
738
- }
739
- if (input.metadata !== undefined) {
740
- sets.push("metadata = ?");
741
- params.push(metadataJson);
666
+ const safeValue = redactSecrets(input.value);
667
+ const safeSummary = input.summary ? redactSecrets(input.summary) : null;
668
+ if (dedupeMode === "merge") {
669
+ const existing = d.query(`SELECT id, version FROM memories
670
+ WHERE key = ? AND scope = ?
671
+ AND COALESCE(agent_id, '') = ?
672
+ AND COALESCE(project_id, '') = ?
673
+ AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
674
+ if (existing) {
675
+ d.run(`UPDATE memories SET
676
+ value = ?, category = ?, summary = ?, tags = ?,
677
+ importance = ?, metadata = ?, expires_at = ?,
678
+ pinned = COALESCE(pinned, 0),
679
+ version = version + 1, updated_at = ?
680
+ WHERE id = ?`, [
681
+ safeValue,
682
+ input.category || "knowledge",
683
+ safeSummary,
684
+ tagsJson,
685
+ input.importance ?? 5,
686
+ metadataJson,
687
+ expiresAt,
688
+ timestamp,
689
+ existing.id
690
+ ]);
691
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
692
+ const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
693
+ for (const tag of tags) {
694
+ insertTag2.run(existing.id, tag);
695
+ }
696
+ const merged = getMemory(existing.id, d);
697
+ try {
698
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
699
+ for (const link of oldLinks) {
700
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
701
+ }
702
+ runEntityExtraction(merged, input.project_id, d);
703
+ } catch {}
704
+ return merged;
742
705
  }
743
- const existingId = existing["id"];
744
- params.push(existingId);
745
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
746
- return getEntity(existingId, d);
747
706
  }
748
- const id = shortUuid();
749
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
750
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
707
+ d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, metadata, access_count, version, expires_at, created_at, updated_at)
708
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
751
709
  id,
752
- input.name,
753
- input.type,
754
- input.description || null,
755
- metadataJson,
710
+ input.key,
711
+ input.value,
712
+ input.category || "knowledge",
713
+ input.scope || "private",
714
+ input.summary || null,
715
+ tagsJson,
716
+ input.importance ?? 5,
717
+ input.source || "agent",
718
+ input.agent_id || null,
756
719
  input.project_id || null,
720
+ input.session_id || null,
721
+ metadataJson,
722
+ expiresAt,
757
723
  timestamp,
758
724
  timestamp
759
725
  ]);
760
- return getEntity(id, d);
726
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
727
+ for (const tag of tags) {
728
+ insertTag.run(id, tag);
729
+ }
730
+ const memory = getMemory(id, d);
731
+ runEntityExtraction(memory, input.project_id, d);
732
+ hookRegistry.runHooks("PostMemorySave", {
733
+ memory,
734
+ wasUpdated: false,
735
+ agentId: input.agent_id,
736
+ projectId: input.project_id,
737
+ sessionId: input.session_id,
738
+ timestamp: Date.now()
739
+ });
740
+ return memory;
761
741
  }
762
- function getEntity(id, db) {
742
+ function getMemory(id, db) {
763
743
  const d = db || getDatabase();
764
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
744
+ const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
765
745
  if (!row)
766
- throw new EntityNotFoundError(id);
767
- return parseEntityRow(row);
746
+ return null;
747
+ return parseMemoryRow(row);
768
748
  }
769
- function getEntityByName(name, type, projectId, db) {
749
+ function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
770
750
  const d = db || getDatabase();
771
- let sql = "SELECT * FROM entities WHERE name = ?";
772
- const params = [name];
773
- if (type) {
774
- sql += " AND type = ?";
775
- params.push(type);
751
+ let sql = "SELECT * FROM memories WHERE key = ?";
752
+ const params = [key];
753
+ if (scope) {
754
+ sql += " AND scope = ?";
755
+ params.push(scope);
776
756
  }
777
- if (projectId !== undefined) {
757
+ if (agentId) {
758
+ sql += " AND agent_id = ?";
759
+ params.push(agentId);
760
+ }
761
+ if (projectId) {
778
762
  sql += " AND project_id = ?";
779
763
  params.push(projectId);
780
764
  }
781
- sql += " LIMIT 1";
765
+ if (sessionId) {
766
+ sql += " AND session_id = ?";
767
+ params.push(sessionId);
768
+ }
769
+ sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
782
770
  const row = d.query(sql).get(...params);
783
771
  if (!row)
784
772
  return null;
785
- return parseEntityRow(row);
773
+ return parseMemoryRow(row);
786
774
  }
787
- function listEntities(filter = {}, db) {
775
+ function getMemoriesByKey(key, scope, agentId, projectId, db) {
788
776
  const d = db || getDatabase();
789
- const conditions = [];
790
- const params = [];
791
- if (filter.type) {
792
- conditions.push("type = ?");
793
- params.push(filter.type);
777
+ let sql = "SELECT * FROM memories WHERE key = ?";
778
+ const params = [key];
779
+ if (scope) {
780
+ sql += " AND scope = ?";
781
+ params.push(scope);
794
782
  }
795
- if (filter.project_id) {
796
- conditions.push("project_id = ?");
797
- params.push(filter.project_id);
783
+ if (agentId) {
784
+ sql += " AND agent_id = ?";
785
+ params.push(agentId);
798
786
  }
799
- if (filter.search) {
800
- conditions.push("(name LIKE ? OR description LIKE ?)");
801
- const term = `%${filter.search}%`;
802
- params.push(term, term);
787
+ if (projectId) {
788
+ sql += " AND project_id = ?";
789
+ params.push(projectId);
803
790
  }
804
- let sql = "SELECT * FROM entities";
791
+ sql += " AND status = 'active' ORDER BY importance DESC";
792
+ const rows = d.query(sql).all(...params);
793
+ return rows.map(parseMemoryRow);
794
+ }
795
+ function listMemories(filter, db) {
796
+ const d = db || getDatabase();
797
+ const conditions = [];
798
+ const params = [];
799
+ if (filter) {
800
+ if (filter.scope) {
801
+ if (Array.isArray(filter.scope)) {
802
+ conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
803
+ params.push(...filter.scope);
804
+ } else {
805
+ conditions.push("scope = ?");
806
+ params.push(filter.scope);
807
+ }
808
+ }
809
+ if (filter.category) {
810
+ if (Array.isArray(filter.category)) {
811
+ conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
812
+ params.push(...filter.category);
813
+ } else {
814
+ conditions.push("category = ?");
815
+ params.push(filter.category);
816
+ }
817
+ }
818
+ if (filter.source) {
819
+ if (Array.isArray(filter.source)) {
820
+ conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
821
+ params.push(...filter.source);
822
+ } else {
823
+ conditions.push("source = ?");
824
+ params.push(filter.source);
825
+ }
826
+ }
827
+ if (filter.status) {
828
+ if (Array.isArray(filter.status)) {
829
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
830
+ params.push(...filter.status);
831
+ } else {
832
+ conditions.push("status = ?");
833
+ params.push(filter.status);
834
+ }
835
+ } else {
836
+ conditions.push("status = 'active'");
837
+ }
838
+ if (filter.project_id) {
839
+ conditions.push("project_id = ?");
840
+ params.push(filter.project_id);
841
+ }
842
+ if (filter.agent_id) {
843
+ conditions.push("agent_id = ?");
844
+ params.push(filter.agent_id);
845
+ }
846
+ if (filter.session_id) {
847
+ conditions.push("session_id = ?");
848
+ params.push(filter.session_id);
849
+ }
850
+ if (filter.min_importance) {
851
+ conditions.push("importance >= ?");
852
+ params.push(filter.min_importance);
853
+ }
854
+ if (filter.pinned !== undefined) {
855
+ conditions.push("pinned = ?");
856
+ params.push(filter.pinned ? 1 : 0);
857
+ }
858
+ if (filter.tags && filter.tags.length > 0) {
859
+ for (const tag of filter.tags) {
860
+ conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
861
+ params.push(tag);
862
+ }
863
+ }
864
+ if (filter.search) {
865
+ conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
866
+ const term = `%${filter.search}%`;
867
+ params.push(term, term, term);
868
+ }
869
+ } else {
870
+ conditions.push("status = 'active'");
871
+ }
872
+ let sql = "SELECT * FROM memories";
805
873
  if (conditions.length > 0) {
806
874
  sql += ` WHERE ${conditions.join(" AND ")}`;
807
875
  }
808
- sql += " ORDER BY updated_at DESC";
809
- if (filter.limit) {
876
+ sql += " ORDER BY importance DESC, created_at DESC";
877
+ if (filter?.limit) {
810
878
  sql += " LIMIT ?";
811
879
  params.push(filter.limit);
812
880
  }
813
- if (filter.offset) {
881
+ if (filter?.offset) {
814
882
  sql += " OFFSET ?";
815
883
  params.push(filter.offset);
816
884
  }
817
885
  const rows = d.query(sql).all(...params);
818
- return rows.map(parseEntityRow);
886
+ return rows.map(parseMemoryRow);
819
887
  }
820
- function updateEntity(id, input, db) {
888
+ function updateMemory(id, input, db) {
821
889
  const d = db || getDatabase();
822
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
890
+ const existing = getMemory(id, d);
823
891
  if (!existing)
824
- throw new EntityNotFoundError(id);
825
- const sets = ["updated_at = ?"];
892
+ throw new MemoryNotFoundError(id);
893
+ if (existing.version !== input.version) {
894
+ throw new VersionConflictError(id, input.version, existing.version);
895
+ }
896
+ try {
897
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
898
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
899
+ uuid(),
900
+ existing.id,
901
+ existing.version,
902
+ existing.value,
903
+ existing.importance,
904
+ existing.scope,
905
+ existing.category,
906
+ JSON.stringify(existing.tags),
907
+ existing.summary,
908
+ existing.pinned ? 1 : 0,
909
+ existing.status,
910
+ existing.updated_at
911
+ ]);
912
+ } catch {}
913
+ const sets = ["version = version + 1", "updated_at = ?"];
826
914
  const params = [now()];
827
- if (input.name !== undefined) {
828
- sets.push("name = ?");
829
- params.push(input.name);
915
+ if (input.value !== undefined) {
916
+ sets.push("value = ?");
917
+ params.push(redactSecrets(input.value));
830
918
  }
831
- if (input.type !== undefined) {
832
- sets.push("type = ?");
833
- params.push(input.type);
919
+ if (input.category !== undefined) {
920
+ sets.push("category = ?");
921
+ params.push(input.category);
834
922
  }
835
- if (input.description !== undefined) {
836
- sets.push("description = ?");
837
- params.push(input.description);
923
+ if (input.scope !== undefined) {
924
+ sets.push("scope = ?");
925
+ params.push(input.scope);
926
+ }
927
+ if (input.summary !== undefined) {
928
+ sets.push("summary = ?");
929
+ params.push(input.summary);
930
+ }
931
+ if (input.importance !== undefined) {
932
+ sets.push("importance = ?");
933
+ params.push(input.importance);
934
+ }
935
+ if (input.pinned !== undefined) {
936
+ sets.push("pinned = ?");
937
+ params.push(input.pinned ? 1 : 0);
938
+ }
939
+ if (input.status !== undefined) {
940
+ sets.push("status = ?");
941
+ params.push(input.status);
838
942
  }
839
943
  if (input.metadata !== undefined) {
840
944
  sets.push("metadata = ?");
841
945
  params.push(JSON.stringify(input.metadata));
842
946
  }
947
+ if (input.expires_at !== undefined) {
948
+ sets.push("expires_at = ?");
949
+ params.push(input.expires_at);
950
+ }
951
+ if (input.tags !== undefined) {
952
+ sets.push("tags = ?");
953
+ params.push(JSON.stringify(input.tags));
954
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
955
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
956
+ for (const tag of input.tags) {
957
+ insertTag.run(id, tag);
958
+ }
959
+ }
843
960
  params.push(id);
844
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
845
- return getEntity(id, d);
846
- }
847
- function deleteEntity(id, db) {
848
- const d = db || getDatabase();
849
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
850
- if (result.changes === 0)
851
- throw new EntityNotFoundError(id);
852
- }
853
- function mergeEntities(sourceId, targetId, db) {
854
- const d = db || getDatabase();
855
- getEntity(sourceId, d);
856
- getEntity(targetId, d);
857
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
858
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
859
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
860
- sourceId,
861
- sourceId
862
- ]);
863
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
864
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
865
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
866
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
867
- return getEntity(targetId, d);
868
- }
869
-
870
- // src/db/entity-memories.ts
871
- function parseEntityRow2(row) {
872
- return {
873
- id: row["id"],
874
- name: row["name"],
875
- type: row["type"],
876
- description: row["description"] || null,
877
- metadata: JSON.parse(row["metadata"] || "{}"),
878
- project_id: row["project_id"] || null,
879
- created_at: row["created_at"],
880
- updated_at: row["updated_at"]
881
- };
882
- }
883
- function parseEntityMemoryRow(row) {
884
- return {
885
- entity_id: row["entity_id"],
886
- memory_id: row["memory_id"],
887
- role: row["role"],
888
- created_at: row["created_at"]
889
- };
961
+ d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
962
+ const updated = getMemory(id, d);
963
+ if (input.value !== undefined) {
964
+ try {
965
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
966
+ for (const link of oldLinks) {
967
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
968
+ }
969
+ } catch {}
970
+ }
971
+ hookRegistry.runHooks("PostMemoryUpdate", {
972
+ memory: updated,
973
+ previousValue: existing.value,
974
+ agentId: existing.agent_id ?? undefined,
975
+ projectId: existing.project_id ?? undefined,
976
+ sessionId: existing.session_id ?? undefined,
977
+ timestamp: Date.now()
978
+ });
979
+ return updated;
890
980
  }
891
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
981
+ function deleteMemory(id, db) {
892
982
  const d = db || getDatabase();
893
- const timestamp = now();
894
- d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
895
- VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
896
- const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
897
- return parseEntityMemoryRow(row);
983
+ const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
984
+ if (result.changes > 0) {
985
+ hookRegistry.runHooks("PostMemoryDelete", {
986
+ memoryId: id,
987
+ timestamp: Date.now()
988
+ });
989
+ }
990
+ return result.changes > 0;
898
991
  }
899
- function unlinkEntityFromMemory(entityId, memoryId, db) {
992
+ function bulkDeleteMemories(ids, db) {
900
993
  const d = db || getDatabase();
901
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
994
+ if (ids.length === 0)
995
+ return 0;
996
+ const placeholders = ids.map(() => "?").join(",");
997
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
998
+ const count = countRow.c;
999
+ if (count > 0) {
1000
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
1001
+ }
1002
+ return count;
902
1003
  }
903
- function getMemoriesForEntity(entityId, db) {
1004
+ function touchMemory(id, db) {
904
1005
  const d = db || getDatabase();
905
- const rows = d.query(`SELECT m.* FROM memories m
906
- INNER JOIN entity_memories em ON em.memory_id = m.id
907
- WHERE em.entity_id = ?
908
- ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
909
- return rows.map(parseMemoryRow);
1006
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
910
1007
  }
911
- function getEntitiesForMemory(memoryId, db) {
1008
+ var RECALL_PROMOTE_THRESHOLD = 3;
1009
+ function incrementRecallCount(id, db) {
912
1010
  const d = db || getDatabase();
913
- const rows = d.query(`SELECT e.* FROM entities e
914
- INNER JOIN entity_memories em ON em.entity_id = e.id
915
- WHERE em.memory_id = ?
916
- ORDER BY e.name ASC`).all(memoryId);
917
- return rows.map(parseEntityRow2);
1011
+ try {
1012
+ d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
1013
+ const row = d.query("SELECT recall_count, importance FROM memories WHERE id = ?").get(id);
1014
+ if (!row)
1015
+ return;
1016
+ const promotions = Math.floor(row.recall_count / RECALL_PROMOTE_THRESHOLD);
1017
+ if (promotions > 0 && row.importance < 10) {
1018
+ const newImportance = Math.min(10, row.importance + 1);
1019
+ d.run("UPDATE memories SET importance = ? WHERE id = ? AND importance < 10", [newImportance, id]);
1020
+ }
1021
+ } catch {}
918
1022
  }
919
- function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
1023
+ function cleanExpiredMemories(db) {
920
1024
  const d = db || getDatabase();
921
1025
  const timestamp = now();
922
- const tx = d.transaction(() => {
923
- const stmt = d.prepare(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
924
- VALUES (?, ?, ?, ?)`);
925
- for (const entityId of entityIds) {
926
- stmt.run(entityId, memoryId, role, timestamp);
927
- }
928
- });
929
- tx();
1026
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1027
+ const count = countRow.c;
1028
+ if (count > 0) {
1029
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1030
+ }
1031
+ return count;
930
1032
  }
931
- function getEntityMemoryLinks(entityId, memoryId, db) {
1033
+ function getMemoryVersions(memoryId, db) {
932
1034
  const d = db || getDatabase();
933
- const conditions = [];
934
- const params = [];
935
- if (entityId) {
936
- conditions.push("entity_id = ?");
937
- params.push(entityId);
938
- }
939
- if (memoryId) {
940
- conditions.push("memory_id = ?");
941
- params.push(memoryId);
942
- }
943
- let sql = "SELECT * FROM entity_memories";
944
- if (conditions.length > 0) {
945
- sql += ` WHERE ${conditions.join(" AND ")}`;
1035
+ try {
1036
+ const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1037
+ return rows.map((row) => ({
1038
+ id: row["id"],
1039
+ memory_id: row["memory_id"],
1040
+ version: row["version"],
1041
+ value: row["value"],
1042
+ importance: row["importance"],
1043
+ scope: row["scope"],
1044
+ category: row["category"],
1045
+ tags: JSON.parse(row["tags"] || "[]"),
1046
+ summary: row["summary"] || null,
1047
+ pinned: !!row["pinned"],
1048
+ status: row["status"],
1049
+ created_at: row["created_at"]
1050
+ }));
1051
+ } catch {
1052
+ return [];
946
1053
  }
947
- sql += " ORDER BY created_at DESC";
948
- const rows = d.query(sql).all(...params);
949
- return rows.map(parseEntityMemoryRow);
950
- }
951
-
952
- // src/db/relations.ts
953
- function parseRelationRow(row) {
954
- return {
955
- id: row["id"],
956
- source_entity_id: row["source_entity_id"],
957
- target_entity_id: row["target_entity_id"],
958
- relation_type: row["relation_type"],
959
- weight: row["weight"],
960
- metadata: JSON.parse(row["metadata"] || "{}"),
961
- created_at: row["created_at"]
962
- };
963
1054
  }
964
- function parseEntityRow3(row) {
1055
+ // src/db/agents.ts
1056
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
1057
+ function parseAgentRow(row) {
965
1058
  return {
966
1059
  id: row["id"],
967
1060
  name: row["name"],
968
- type: row["type"],
1061
+ session_id: row["session_id"] || null,
969
1062
  description: row["description"] || null,
1063
+ role: row["role"] || null,
970
1064
  metadata: JSON.parse(row["metadata"] || "{}"),
971
- project_id: row["project_id"] || null,
1065
+ active_project_id: row["active_project_id"] || null,
972
1066
  created_at: row["created_at"],
973
- updated_at: row["updated_at"]
1067
+ last_seen_at: row["last_seen_at"]
974
1068
  };
975
1069
  }
976
- function createRelation(input, db) {
1070
+ function registerAgent(name, sessionId, description, role, projectId, db) {
977
1071
  const d = db || getDatabase();
978
- const id = shortUuid();
979
1072
  const timestamp = now();
980
- const weight = input.weight ?? 1;
981
- const metadata = JSON.stringify(input.metadata ?? {});
982
- d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
983
- VALUES (?, ?, ?, ?, ?, ?, ?)
984
- ON CONFLICT(source_entity_id, target_entity_id, relation_type)
985
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
986
- const row = d.query(`SELECT * FROM relations
987
- WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
988
- return parseRelationRow(row);
1073
+ const normalizedName = name.trim().toLowerCase();
1074
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
1075
+ if (existing) {
1076
+ const existingId = existing["id"];
1077
+ const existingSessionId = existing["session_id"] || null;
1078
+ const existingLastSeen = existing["last_seen_at"];
1079
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
1080
+ const lastSeenMs = new Date(existingLastSeen).getTime();
1081
+ const nowMs = Date.now();
1082
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
1083
+ throw new AgentConflictError({
1084
+ existing_id: existingId,
1085
+ existing_name: normalizedName,
1086
+ last_seen_at: existingLastSeen,
1087
+ session_hint: existingSessionId.slice(0, 8),
1088
+ working_dir: null
1089
+ });
1090
+ }
1091
+ }
1092
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
1093
+ timestamp,
1094
+ sessionId ?? existingSessionId,
1095
+ existingId
1096
+ ]);
1097
+ if (description) {
1098
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
1099
+ }
1100
+ if (role) {
1101
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
1102
+ }
1103
+ if (projectId !== undefined) {
1104
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
1105
+ }
1106
+ return getAgent(existingId, d);
1107
+ }
1108
+ const id = shortUuid();
1109
+ d.run("INSERT INTO agents (id, name, session_id, description, role, active_project_id, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, normalizedName, sessionId ?? null, description || null, role || "agent", projectId ?? null, timestamp, timestamp]);
1110
+ return getAgent(id, d);
989
1111
  }
990
- function getRelation(id, db) {
1112
+ function getAgent(idOrName, db) {
991
1113
  const d = db || getDatabase();
992
- const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
993
- if (!row)
994
- throw new Error(`Relation not found: ${id}`);
995
- return parseRelationRow(row);
1114
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
1115
+ if (row)
1116
+ return parseAgentRow(row);
1117
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
1118
+ if (row)
1119
+ return parseAgentRow(row);
1120
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
1121
+ if (rows.length === 1)
1122
+ return parseAgentRow(rows[0]);
1123
+ return null;
996
1124
  }
997
- function listRelations(filter, db) {
1125
+ function listAgents(db) {
998
1126
  const d = db || getDatabase();
999
- const conditions = [];
1000
- const params = [];
1001
- if (filter.entity_id) {
1002
- const dir = filter.direction || "both";
1003
- if (dir === "outgoing") {
1004
- conditions.push("source_entity_id = ?");
1005
- params.push(filter.entity_id);
1006
- } else if (dir === "incoming") {
1007
- conditions.push("target_entity_id = ?");
1008
- params.push(filter.entity_id);
1009
- } else {
1010
- conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
1011
- params.push(filter.entity_id, filter.entity_id);
1127
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
1128
+ return rows.map(parseAgentRow);
1129
+ }
1130
+ function touchAgent(idOrName, db) {
1131
+ const d = db || getDatabase();
1132
+ const agent = getAgent(idOrName, d);
1133
+ if (!agent)
1134
+ return;
1135
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
1136
+ }
1137
+ function listAgentsByProject(projectId, db) {
1138
+ const d = db || getDatabase();
1139
+ const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
1140
+ return rows.map(parseAgentRow);
1141
+ }
1142
+ function updateAgent(id, updates, db) {
1143
+ const d = db || getDatabase();
1144
+ const agent = getAgent(id, d);
1145
+ if (!agent)
1146
+ return null;
1147
+ const timestamp = now();
1148
+ if (updates.name) {
1149
+ const normalizedNewName = updates.name.trim().toLowerCase();
1150
+ if (normalizedNewName !== agent.name) {
1151
+ const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
1152
+ if (existing) {
1153
+ throw new Error(`Agent name already taken: ${normalizedNewName}`);
1154
+ }
1155
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
1012
1156
  }
1013
1157
  }
1014
- if (filter.relation_type) {
1015
- conditions.push("relation_type = ?");
1016
- params.push(filter.relation_type);
1158
+ if (updates.description !== undefined) {
1159
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
1017
1160
  }
1018
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1019
- const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
1020
- return rows.map(parseRelationRow);
1161
+ if (updates.role !== undefined) {
1162
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
1163
+ }
1164
+ if (updates.metadata !== undefined) {
1165
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
1166
+ }
1167
+ if ("active_project_id" in updates) {
1168
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
1169
+ }
1170
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
1171
+ return getAgent(agent.id, d);
1021
1172
  }
1022
- function deleteRelation(id, db) {
1023
- const d = db || getDatabase();
1024
- const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
1025
- if (result.changes === 0)
1026
- throw new Error(`Relation not found: ${id}`);
1173
+ // src/db/locks.ts
1174
+ function parseLockRow(row) {
1175
+ return {
1176
+ id: row["id"],
1177
+ resource_type: row["resource_type"],
1178
+ resource_id: row["resource_id"],
1179
+ agent_id: row["agent_id"],
1180
+ lock_type: row["lock_type"],
1181
+ locked_at: row["locked_at"],
1182
+ expires_at: row["expires_at"]
1183
+ };
1027
1184
  }
1028
- function getRelatedEntities(entityId, relationType, db) {
1185
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
1029
1186
  const d = db || getDatabase();
1030
- let sql;
1031
- const params = [];
1032
- if (relationType) {
1033
- sql = `
1034
- SELECT DISTINCT e.* FROM entities e
1035
- JOIN relations r ON (
1036
- (r.source_entity_id = ? AND r.target_entity_id = e.id)
1037
- OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
1038
- )
1039
- WHERE r.relation_type = ?
1040
- `;
1041
- params.push(entityId, entityId, relationType);
1042
- } else {
1043
- sql = `
1044
- SELECT DISTINCT e.* FROM entities e
1045
- JOIN relations r ON (
1046
- (r.source_entity_id = ? AND r.target_entity_id = e.id)
1047
- OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
1048
- )
1049
- `;
1050
- params.push(entityId, entityId);
1187
+ cleanExpiredLocks(d);
1188
+ const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
1189
+ if (ownLock) {
1190
+ const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1191
+ d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
1192
+ newExpiry,
1193
+ ownLock["id"]
1194
+ ]);
1195
+ return parseLockRow({ ...ownLock, expires_at: newExpiry });
1051
1196
  }
1052
- const rows = d.query(sql).all(...params);
1053
- return rows.map(parseEntityRow3);
1197
+ if (lockType === "exclusive") {
1198
+ const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
1199
+ if (existing) {
1200
+ return null;
1201
+ }
1202
+ }
1203
+ const id = shortUuid();
1204
+ const lockedAt = now();
1205
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1206
+ d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
1207
+ return {
1208
+ id,
1209
+ resource_type: resourceType,
1210
+ resource_id: resourceId,
1211
+ agent_id: agentId,
1212
+ lock_type: lockType,
1213
+ locked_at: lockedAt,
1214
+ expires_at: expiresAt
1215
+ };
1054
1216
  }
1055
- function getEntityGraph(entityId, depth = 2, db) {
1217
+ function releaseLock(lockId, agentId, db) {
1056
1218
  const d = db || getDatabase();
1057
- const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
1058
- VALUES(?, 0)
1059
- UNION
1060
- SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
1061
- FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
1062
- WHERE g.depth < ?
1063
- )
1064
- SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
1065
- const entities = entityRows.map(parseEntityRow3);
1066
- const entityIds = new Set(entities.map((e) => e.id));
1067
- if (entityIds.size === 0) {
1068
- return { entities: [], relations: [] };
1069
- }
1070
- const placeholders = Array.from(entityIds).map(() => "?").join(",");
1071
- const relationRows = d.query(`SELECT * FROM relations
1072
- WHERE source_entity_id IN (${placeholders})
1073
- AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
1074
- const relations = relationRows.map(parseRelationRow);
1075
- return { entities, relations };
1219
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1220
+ return result.changes > 0;
1076
1221
  }
1077
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
1222
+ function releaseResourceLocks(agentId, resourceType, resourceId, db) {
1078
1223
  const d = db || getDatabase();
1079
- const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
1080
- SELECT ?, ?, 0
1081
- UNION
1082
- SELECT
1083
- CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1084
- p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1085
- p.depth + 1
1086
- FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
1087
- WHERE p.depth < ?
1088
- AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
1089
- )
1090
- SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
1091
- if (!rows)
1092
- return null;
1093
- const ids = rows.trail.split(",");
1094
- const entities = [];
1095
- for (const id of ids) {
1096
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1097
- if (row)
1098
- entities.push(parseEntityRow3(row));
1099
- }
1100
- return entities.length > 0 ? entities : null;
1224
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ?", [agentId, resourceType, resourceId]);
1225
+ return result.changes;
1101
1226
  }
1102
-
1103
- // src/lib/config.ts
1104
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1105
- import { homedir } from "os";
1106
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1107
- var DEFAULT_CONFIG = {
1108
- default_scope: "private",
1109
- default_category: "knowledge",
1110
- default_importance: 5,
1111
- max_entries: 1000,
1112
- max_entries_per_scope: {
1113
- global: 500,
1114
- shared: 300,
1115
- private: 200
1116
- },
1117
- injection: {
1118
- max_tokens: 500,
1119
- min_importance: 5,
1120
- categories: ["preference", "fact"],
1121
- refresh_interval: 5
1122
- },
1123
- extraction: {
1124
- enabled: true,
1125
- min_confidence: 0.5
1126
- },
1127
- sync_agents: ["claude", "codex", "gemini"],
1128
- auto_cleanup: {
1129
- enabled: true,
1130
- expired_check_interval: 3600,
1131
- unused_archive_days: 7,
1132
- stale_deprioritize_days: 14
1133
- }
1134
- };
1135
- function deepMerge(target, source) {
1136
- const result = { ...target };
1137
- for (const key of Object.keys(source)) {
1138
- const sourceVal = source[key];
1139
- const targetVal = result[key];
1140
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
1141
- result[key] = deepMerge(targetVal, sourceVal);
1142
- } else {
1143
- result[key] = sourceVal;
1144
- }
1145
- }
1146
- return result;
1227
+ function releaseAllAgentLocks(agentId, db) {
1228
+ const d = db || getDatabase();
1229
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1230
+ return result.changes;
1147
1231
  }
1148
- var VALID_SCOPES = ["global", "shared", "private"];
1149
- var VALID_CATEGORIES = [
1150
- "preference",
1151
- "fact",
1152
- "knowledge",
1153
- "history"
1154
- ];
1155
- function isValidScope(value) {
1156
- return VALID_SCOPES.includes(value);
1232
+ function checkLock(resourceType, resourceId, lockType, db) {
1233
+ const d = db || getDatabase();
1234
+ cleanExpiredLocks(d);
1235
+ const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1236
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
1237
+ return rows.map(parseLockRow);
1157
1238
  }
1158
- function isValidCategory(value) {
1159
- return VALID_CATEGORIES.includes(value);
1239
+ function agentHoldsLock(agentId, resourceType, resourceId, lockType, db) {
1240
+ const d = db || getDatabase();
1241
+ const query = lockType ? "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1242
+ const row = lockType ? d.query(query).get(agentId, resourceType, resourceId, lockType) : d.query(query).get(agentId, resourceType, resourceId);
1243
+ return row ? parseLockRow(row) : null;
1160
1244
  }
1161
- function loadConfig() {
1162
- const configPath = join2(homedir(), ".mementos", "config.json");
1163
- let fileConfig = {};
1164
- if (existsSync2(configPath)) {
1165
- try {
1166
- const raw = readFileSync(configPath, "utf-8");
1167
- fileConfig = JSON.parse(raw);
1168
- } catch {}
1169
- }
1170
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
1171
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
1172
- if (envScope && isValidScope(envScope)) {
1173
- merged.default_scope = envScope;
1174
- }
1175
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
1176
- if (envCategory && isValidCategory(envCategory)) {
1177
- merged.default_category = envCategory;
1178
- }
1179
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
1180
- if (envImportance) {
1181
- const parsed = parseInt(envImportance, 10);
1182
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
1183
- merged.default_importance = parsed;
1184
- }
1185
- }
1186
- return merged;
1245
+ function listAgentLocks(agentId, db) {
1246
+ const d = db || getDatabase();
1247
+ cleanExpiredLocks(d);
1248
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
1249
+ return rows.map(parseLockRow);
1187
1250
  }
1188
- function profilesDir() {
1189
- return join2(homedir(), ".mementos", "profiles");
1251
+ function cleanExpiredLocks(db) {
1252
+ const d = db || getDatabase();
1253
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
1254
+ return result.changes;
1190
1255
  }
1191
- function globalConfigPath() {
1192
- return join2(homedir(), ".mementos", "config.json");
1256
+ // src/lib/memory-lock.ts
1257
+ var MEMORY_WRITE_TTL = 30;
1258
+ function memoryLockId(key, scope, projectId) {
1259
+ return `${scope}:${key}:${projectId ?? ""}`;
1193
1260
  }
1194
- function readGlobalConfig() {
1195
- const p = globalConfigPath();
1196
- if (!existsSync2(p))
1197
- return {};
1198
- try {
1199
- return JSON.parse(readFileSync(p, "utf-8"));
1200
- } catch {
1201
- return {};
1202
- }
1261
+ function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
1262
+ const d = db || getDatabase();
1263
+ return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
1203
1264
  }
1204
- function writeGlobalConfig(data) {
1205
- const p = globalConfigPath();
1206
- ensureDir2(dirname2(p));
1207
- writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
1265
+ function releaseMemoryWriteLock(lockId, agentId, db) {
1266
+ const d = db || getDatabase();
1267
+ return releaseLock(lockId, agentId, d);
1208
1268
  }
1209
- function getActiveProfile() {
1210
- const envProfile = process.env["MEMENTOS_PROFILE"];
1211
- if (envProfile)
1212
- return envProfile.trim();
1213
- const cfg = readGlobalConfig();
1214
- return cfg["active_profile"] || null;
1269
+ function checkMemoryWriteLock(key, scope, projectId, db) {
1270
+ const d = db || getDatabase();
1271
+ const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
1272
+ return locks[0] ?? null;
1215
1273
  }
1216
- function setActiveProfile(name) {
1217
- const cfg = readGlobalConfig();
1218
- if (name === null) {
1219
- delete cfg["active_profile"];
1220
- } else {
1221
- cfg["active_profile"] = name;
1274
+ function withMemoryLock(agentId, key, scope, projectId, fn, ttlSeconds = MEMORY_WRITE_TTL, db) {
1275
+ const d = db || getDatabase();
1276
+ const lock = acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds, d);
1277
+ if (!lock) {
1278
+ const existing = checkMemoryWriteLock(key, scope, projectId, d);
1279
+ throw new MemoryLockConflictError(key, scope, existing?.agent_id ?? "unknown");
1280
+ }
1281
+ try {
1282
+ return fn();
1283
+ } finally {
1284
+ releaseLock(lock.id, agentId, d);
1222
1285
  }
1223
- writeGlobalConfig(cfg);
1224
1286
  }
1225
- function listProfiles() {
1226
- const dir = profilesDir();
1227
- if (!existsSync2(dir))
1228
- return [];
1229
- return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
1287
+
1288
+ class MemoryLockConflictError extends Error {
1289
+ conflict = true;
1290
+ key;
1291
+ scope;
1292
+ blocking_agent_id;
1293
+ constructor(key, scope, blockingAgentId) {
1294
+ super(`Memory key "${key}" (scope: ${scope}) is currently write-locked by agent ${blockingAgentId}. ` + "Retry after a few seconds or use optimistic locking (version field).");
1295
+ this.name = "MemoryLockConflictError";
1296
+ this.key = key;
1297
+ this.scope = scope;
1298
+ this.blocking_agent_id = blockingAgentId;
1299
+ }
1230
1300
  }
1231
- function deleteProfile(name) {
1232
- const dbPath = join2(profilesDir(), `${name}.db`);
1233
- if (!existsSync2(dbPath))
1234
- return false;
1235
- unlinkSync(dbPath);
1236
- if (getActiveProfile() === name)
1237
- setActiveProfile(null);
1238
- return true;
1301
+ // src/lib/focus.ts
1302
+ var sessionFocus = new Map;
1303
+ function setFocus(agentId, projectId) {
1304
+ const previous = getFocusCached(agentId);
1305
+ sessionFocus.set(agentId, projectId);
1306
+ updateAgent(agentId, { active_project_id: projectId });
1307
+ if (projectId && projectId !== previous) {
1308
+ hookRegistry.runHooks("OnSessionStart", {
1309
+ agentId,
1310
+ projectId,
1311
+ timestamp: Date.now()
1312
+ });
1313
+ } else if (!projectId && previous) {
1314
+ hookRegistry.runHooks("OnSessionEnd", {
1315
+ agentId,
1316
+ projectId: previous,
1317
+ timestamp: Date.now()
1318
+ });
1319
+ }
1239
1320
  }
1240
- function ensureDir2(dir) {
1241
- if (!existsSync2(dir)) {
1242
- mkdirSync2(dir, { recursive: true });
1321
+ function getFocusCached(agentId) {
1322
+ return sessionFocus.get(agentId) ?? null;
1323
+ }
1324
+ function getFocus(agentId) {
1325
+ if (sessionFocus.has(agentId)) {
1326
+ return sessionFocus.get(agentId) ?? null;
1243
1327
  }
1328
+ const agent = getAgent(agentId);
1329
+ const projectId = agent?.active_project_id ?? null;
1330
+ sessionFocus.set(agentId, projectId);
1331
+ return projectId;
1244
1332
  }
1245
-
1246
- // src/db/memories.ts
1247
- function runEntityExtraction(memory, projectId, d) {
1248
- const config = loadConfig();
1249
- if (config.extraction?.enabled === false)
1250
- return;
1251
- const extracted = extractEntities(memory, d);
1252
- const minConfidence = config.extraction?.min_confidence ?? 0.5;
1253
- const entityIds = [];
1254
- for (const ext of extracted) {
1255
- if (ext.confidence >= minConfidence) {
1256
- const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
1257
- linkEntityToMemory(entity.id, memory.id, "context", d);
1258
- entityIds.push(entity.id);
1259
- }
1260
- }
1261
- for (let i = 0;i < entityIds.length; i++) {
1262
- for (let j = i + 1;j < entityIds.length; j++) {
1263
- try {
1264
- createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
1265
- } catch {}
1266
- }
1333
+ function unfocus(agentId) {
1334
+ setFocus(agentId, null);
1335
+ }
1336
+ function resolveProjectId(agentId, explicitProjectId) {
1337
+ if (explicitProjectId !== undefined && explicitProjectId !== null) {
1338
+ return explicitProjectId;
1339
+ }
1340
+ if (agentId) {
1341
+ return getFocus(agentId);
1267
1342
  }
1343
+ return null;
1268
1344
  }
1269
- function parseMemoryRow(row) {
1345
+ function buildFocusFilter(agentId, explicitProjectId, explicitScope) {
1346
+ if (explicitScope || explicitProjectId)
1347
+ return null;
1348
+ if (!agentId)
1349
+ return null;
1350
+ const focusedProjectId = getFocus(agentId);
1351
+ if (!focusedProjectId)
1352
+ return null;
1353
+ return {
1354
+ focusMode: true,
1355
+ agentId,
1356
+ projectId: focusedProjectId
1357
+ };
1358
+ }
1359
+ function focusFilterSQL(agentId, projectId) {
1360
+ return {
1361
+ sql: "(scope = 'global' OR (scope = 'private' AND agent_id = ?) OR (scope = 'shared' AND project_id = ?))",
1362
+ params: [agentId, projectId]
1363
+ };
1364
+ }
1365
+ // src/db/projects.ts
1366
+ function parseProjectRow(row) {
1270
1367
  return {
1271
1368
  id: row["id"],
1272
- key: row["key"],
1273
- value: row["value"],
1274
- category: row["category"],
1275
- scope: row["scope"],
1276
- summary: row["summary"] || null,
1277
- tags: JSON.parse(row["tags"] || "[]"),
1278
- importance: row["importance"],
1279
- source: row["source"],
1280
- status: row["status"],
1281
- pinned: !!row["pinned"],
1282
- agent_id: row["agent_id"] || null,
1283
- project_id: row["project_id"] || null,
1284
- session_id: row["session_id"] || null,
1285
- metadata: JSON.parse(row["metadata"] || "{}"),
1286
- access_count: row["access_count"],
1287
- version: row["version"],
1288
- expires_at: row["expires_at"] || null,
1369
+ name: row["name"],
1370
+ path: row["path"],
1371
+ description: row["description"] || null,
1372
+ memory_prefix: row["memory_prefix"] || null,
1289
1373
  created_at: row["created_at"],
1290
- updated_at: row["updated_at"],
1291
- accessed_at: row["accessed_at"] || null
1374
+ updated_at: row["updated_at"]
1292
1375
  };
1293
1376
  }
1294
- function createMemory(input, dedupeMode = "merge", db) {
1377
+ function registerProject(name, path, description, memoryPrefix, db) {
1295
1378
  const d = db || getDatabase();
1296
1379
  const timestamp = now();
1297
- let expiresAt = input.expires_at || null;
1298
- if (input.ttl_ms && !expiresAt) {
1299
- expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
1380
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
1381
+ if (existing) {
1382
+ const existingId = existing["id"];
1383
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
1384
+ timestamp,
1385
+ existingId
1386
+ ]);
1387
+ return parseProjectRow(existing);
1300
1388
  }
1301
1389
  const id = uuid();
1302
- const tags = input.tags || [];
1303
- const tagsJson = JSON.stringify(tags);
1304
- const metadataJson = JSON.stringify(input.metadata || {});
1305
- const safeValue = redactSecrets(input.value);
1306
- const safeSummary = input.summary ? redactSecrets(input.summary) : null;
1307
- if (dedupeMode === "merge") {
1308
- const existing = d.query(`SELECT id, version FROM memories
1309
- WHERE key = ? AND scope = ?
1310
- AND COALESCE(agent_id, '') = ?
1311
- AND COALESCE(project_id, '') = ?
1312
- AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
1313
- if (existing) {
1314
- d.run(`UPDATE memories SET
1315
- value = ?, category = ?, summary = ?, tags = ?,
1316
- importance = ?, metadata = ?, expires_at = ?,
1317
- pinned = COALESCE(pinned, 0),
1318
- version = version + 1, updated_at = ?
1319
- WHERE id = ?`, [
1320
- safeValue,
1321
- input.category || "knowledge",
1322
- safeSummary,
1323
- tagsJson,
1324
- input.importance ?? 5,
1325
- metadataJson,
1326
- expiresAt,
1327
- timestamp,
1328
- existing.id
1329
- ]);
1330
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
1331
- const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1332
- for (const tag of tags) {
1333
- insertTag2.run(existing.id, tag);
1334
- }
1335
- const merged = getMemory(existing.id, d);
1336
- try {
1337
- const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
1338
- for (const link of oldLinks) {
1339
- unlinkEntityFromMemory(link.entity_id, merged.id, d);
1340
- }
1341
- runEntityExtraction(merged, input.project_id, d);
1342
- } catch {}
1343
- return merged;
1390
+ d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
1391
+ return getProject(id, d);
1392
+ }
1393
+ function getProject(idOrPath, db) {
1394
+ const d = db || getDatabase();
1395
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
1396
+ if (row)
1397
+ return parseProjectRow(row);
1398
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
1399
+ if (row)
1400
+ return parseProjectRow(row);
1401
+ row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
1402
+ if (row)
1403
+ return parseProjectRow(row);
1404
+ return null;
1405
+ }
1406
+ function listProjects(db) {
1407
+ const d = db || getDatabase();
1408
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
1409
+ return rows.map(parseProjectRow);
1410
+ }
1411
+ // src/db/entities.ts
1412
+ function parseEntityRow2(row) {
1413
+ return {
1414
+ id: row["id"],
1415
+ name: row["name"],
1416
+ type: row["type"],
1417
+ description: row["description"] || null,
1418
+ metadata: JSON.parse(row["metadata"] || "{}"),
1419
+ project_id: row["project_id"] || null,
1420
+ created_at: row["created_at"],
1421
+ updated_at: row["updated_at"]
1422
+ };
1423
+ }
1424
+ function createEntity(input, db) {
1425
+ const d = db || getDatabase();
1426
+ const timestamp = now();
1427
+ const metadataJson = JSON.stringify(input.metadata || {});
1428
+ const existing = d.query(`SELECT * FROM entities
1429
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
1430
+ if (existing) {
1431
+ const sets = ["updated_at = ?"];
1432
+ const params = [timestamp];
1433
+ if (input.description !== undefined) {
1434
+ sets.push("description = ?");
1435
+ params.push(input.description);
1436
+ }
1437
+ if (input.metadata !== undefined) {
1438
+ sets.push("metadata = ?");
1439
+ params.push(metadataJson);
1344
1440
  }
1441
+ const existingId = existing["id"];
1442
+ params.push(existingId);
1443
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1444
+ return getEntity(existingId, d);
1345
1445
  }
1346
- d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, metadata, access_count, version, expires_at, created_at, updated_at)
1347
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
1446
+ const id = shortUuid();
1447
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
1448
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1348
1449
  id,
1349
- input.key,
1350
- input.value,
1351
- input.category || "knowledge",
1352
- input.scope || "private",
1353
- input.summary || null,
1354
- tagsJson,
1355
- input.importance ?? 5,
1356
- input.source || "agent",
1357
- input.agent_id || null,
1358
- input.project_id || null,
1359
- input.session_id || null,
1450
+ input.name,
1451
+ input.type,
1452
+ input.description || null,
1360
1453
  metadataJson,
1361
- expiresAt,
1454
+ input.project_id || null,
1362
1455
  timestamp,
1363
1456
  timestamp
1364
1457
  ]);
1365
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1366
- for (const tag of tags) {
1367
- insertTag.run(id, tag);
1368
- }
1369
- const memory = getMemory(id, d);
1370
- try {
1371
- runEntityExtraction(memory, input.project_id, d);
1372
- } catch {}
1373
- return memory;
1458
+ hookRegistry.runHooks("PostEntityCreate", {
1459
+ entityId: id,
1460
+ name: input.name,
1461
+ entityType: input.type,
1462
+ projectId: input.project_id,
1463
+ timestamp: Date.now()
1464
+ });
1465
+ return getEntity(id, d);
1374
1466
  }
1375
- function getMemory(id, db) {
1467
+ function getEntity(id, db) {
1376
1468
  const d = db || getDatabase();
1377
- const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
1469
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1378
1470
  if (!row)
1379
- return null;
1380
- return parseMemoryRow(row);
1471
+ throw new EntityNotFoundError(id);
1472
+ return parseEntityRow2(row);
1381
1473
  }
1382
- function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
1474
+ function getEntityByName(name, type, projectId, db) {
1383
1475
  const d = db || getDatabase();
1384
- let sql = "SELECT * FROM memories WHERE key = ?";
1385
- const params = [key];
1386
- if (scope) {
1387
- sql += " AND scope = ?";
1388
- params.push(scope);
1389
- }
1390
- if (agentId) {
1391
- sql += " AND agent_id = ?";
1392
- params.push(agentId);
1476
+ let sql = "SELECT * FROM entities WHERE name = ?";
1477
+ const params = [name];
1478
+ if (type) {
1479
+ sql += " AND type = ?";
1480
+ params.push(type);
1393
1481
  }
1394
- if (projectId) {
1482
+ if (projectId !== undefined) {
1395
1483
  sql += " AND project_id = ?";
1396
1484
  params.push(projectId);
1397
1485
  }
1398
- if (sessionId) {
1399
- sql += " AND session_id = ?";
1400
- params.push(sessionId);
1401
- }
1402
- sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
1486
+ sql += " LIMIT 1";
1403
1487
  const row = d.query(sql).get(...params);
1404
1488
  if (!row)
1405
1489
  return null;
1406
- return parseMemoryRow(row);
1407
- }
1408
- function getMemoriesByKey(key, scope, agentId, projectId, db) {
1409
- const d = db || getDatabase();
1410
- let sql = "SELECT * FROM memories WHERE key = ?";
1411
- const params = [key];
1412
- if (scope) {
1413
- sql += " AND scope = ?";
1414
- params.push(scope);
1415
- }
1416
- if (agentId) {
1417
- sql += " AND agent_id = ?";
1418
- params.push(agentId);
1419
- }
1420
- if (projectId) {
1421
- sql += " AND project_id = ?";
1422
- params.push(projectId);
1423
- }
1424
- sql += " AND status = 'active' ORDER BY importance DESC";
1425
- const rows = d.query(sql).all(...params);
1426
- return rows.map(parseMemoryRow);
1490
+ return parseEntityRow2(row);
1427
1491
  }
1428
- function listMemories(filter, db) {
1492
+ function listEntities(filter = {}, db) {
1429
1493
  const d = db || getDatabase();
1430
1494
  const conditions = [];
1431
1495
  const params = [];
1432
- if (filter) {
1433
- if (filter.scope) {
1434
- if (Array.isArray(filter.scope)) {
1435
- conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
1436
- params.push(...filter.scope);
1437
- } else {
1438
- conditions.push("scope = ?");
1439
- params.push(filter.scope);
1440
- }
1441
- }
1442
- if (filter.category) {
1443
- if (Array.isArray(filter.category)) {
1444
- conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
1445
- params.push(...filter.category);
1446
- } else {
1447
- conditions.push("category = ?");
1448
- params.push(filter.category);
1449
- }
1450
- }
1451
- if (filter.source) {
1452
- if (Array.isArray(filter.source)) {
1453
- conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
1454
- params.push(...filter.source);
1455
- } else {
1456
- conditions.push("source = ?");
1457
- params.push(filter.source);
1458
- }
1459
- }
1460
- if (filter.status) {
1461
- if (Array.isArray(filter.status)) {
1462
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1463
- params.push(...filter.status);
1464
- } else {
1465
- conditions.push("status = ?");
1466
- params.push(filter.status);
1467
- }
1468
- } else {
1469
- conditions.push("status = 'active'");
1470
- }
1471
- if (filter.project_id) {
1472
- conditions.push("project_id = ?");
1473
- params.push(filter.project_id);
1474
- }
1475
- if (filter.agent_id) {
1476
- conditions.push("agent_id = ?");
1477
- params.push(filter.agent_id);
1478
- }
1479
- if (filter.session_id) {
1480
- conditions.push("session_id = ?");
1481
- params.push(filter.session_id);
1482
- }
1483
- if (filter.min_importance) {
1484
- conditions.push("importance >= ?");
1485
- params.push(filter.min_importance);
1486
- }
1487
- if (filter.pinned !== undefined) {
1488
- conditions.push("pinned = ?");
1489
- params.push(filter.pinned ? 1 : 0);
1490
- }
1491
- if (filter.tags && filter.tags.length > 0) {
1492
- for (const tag of filter.tags) {
1493
- conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1494
- params.push(tag);
1495
- }
1496
- }
1497
- if (filter.search) {
1498
- conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
1499
- const term = `%${filter.search}%`;
1500
- params.push(term, term, term);
1501
- }
1502
- } else {
1503
- conditions.push("status = 'active'");
1496
+ if (filter.type) {
1497
+ conditions.push("type = ?");
1498
+ params.push(filter.type);
1504
1499
  }
1505
- let sql = "SELECT * FROM memories";
1500
+ if (filter.project_id) {
1501
+ conditions.push("project_id = ?");
1502
+ params.push(filter.project_id);
1503
+ }
1504
+ if (filter.search) {
1505
+ conditions.push("(name LIKE ? OR description LIKE ?)");
1506
+ const term = `%${filter.search}%`;
1507
+ params.push(term, term);
1508
+ }
1509
+ let sql = "SELECT * FROM entities";
1506
1510
  if (conditions.length > 0) {
1507
1511
  sql += ` WHERE ${conditions.join(" AND ")}`;
1508
1512
  }
1509
- sql += " ORDER BY importance DESC, created_at DESC";
1510
- if (filter?.limit) {
1511
- sql += " LIMIT ?";
1513
+ sql += " ORDER BY updated_at DESC";
1514
+ if (filter.limit) {
1515
+ sql += " LIMIT ?";
1512
1516
  params.push(filter.limit);
1513
1517
  }
1514
- if (filter?.offset) {
1518
+ if (filter.offset) {
1515
1519
  sql += " OFFSET ?";
1516
1520
  params.push(filter.offset);
1517
1521
  }
1518
1522
  const rows = d.query(sql).all(...params);
1519
- return rows.map(parseMemoryRow);
1523
+ return rows.map(parseEntityRow2);
1520
1524
  }
1521
- function updateMemory(id, input, db) {
1525
+ function updateEntity(id, input, db) {
1522
1526
  const d = db || getDatabase();
1523
- const existing = getMemory(id, d);
1527
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1524
1528
  if (!existing)
1525
- throw new MemoryNotFoundError(id);
1526
- if (existing.version !== input.version) {
1527
- throw new VersionConflictError(id, input.version, existing.version);
1528
- }
1529
- try {
1530
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
1531
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1532
- uuid(),
1533
- existing.id,
1534
- existing.version,
1535
- existing.value,
1536
- existing.importance,
1537
- existing.scope,
1538
- existing.category,
1539
- JSON.stringify(existing.tags),
1540
- existing.summary,
1541
- existing.pinned ? 1 : 0,
1542
- existing.status,
1543
- existing.updated_at
1544
- ]);
1545
- } catch {}
1546
- const sets = ["version = version + 1", "updated_at = ?"];
1529
+ throw new EntityNotFoundError(id);
1530
+ const sets = ["updated_at = ?"];
1547
1531
  const params = [now()];
1548
- if (input.value !== undefined) {
1549
- sets.push("value = ?");
1550
- params.push(redactSecrets(input.value));
1551
- }
1552
- if (input.category !== undefined) {
1553
- sets.push("category = ?");
1554
- params.push(input.category);
1555
- }
1556
- if (input.scope !== undefined) {
1557
- sets.push("scope = ?");
1558
- params.push(input.scope);
1559
- }
1560
- if (input.summary !== undefined) {
1561
- sets.push("summary = ?");
1562
- params.push(input.summary);
1563
- }
1564
- if (input.importance !== undefined) {
1565
- sets.push("importance = ?");
1566
- params.push(input.importance);
1532
+ if (input.name !== undefined) {
1533
+ sets.push("name = ?");
1534
+ params.push(input.name);
1567
1535
  }
1568
- if (input.pinned !== undefined) {
1569
- sets.push("pinned = ?");
1570
- params.push(input.pinned ? 1 : 0);
1536
+ if (input.type !== undefined) {
1537
+ sets.push("type = ?");
1538
+ params.push(input.type);
1571
1539
  }
1572
- if (input.status !== undefined) {
1573
- sets.push("status = ?");
1574
- params.push(input.status);
1540
+ if (input.description !== undefined) {
1541
+ sets.push("description = ?");
1542
+ params.push(input.description);
1575
1543
  }
1576
1544
  if (input.metadata !== undefined) {
1577
1545
  sets.push("metadata = ?");
1578
1546
  params.push(JSON.stringify(input.metadata));
1579
1547
  }
1580
- if (input.expires_at !== undefined) {
1581
- sets.push("expires_at = ?");
1582
- params.push(input.expires_at);
1583
- }
1584
- if (input.tags !== undefined) {
1585
- sets.push("tags = ?");
1586
- params.push(JSON.stringify(input.tags));
1587
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
1588
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1589
- for (const tag of input.tags) {
1590
- insertTag.run(id, tag);
1591
- }
1592
- }
1593
1548
  params.push(id);
1594
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
1595
- const updated = getMemory(id, d);
1596
- try {
1597
- if (input.value !== undefined) {
1598
- const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
1599
- for (const link of oldLinks) {
1600
- unlinkEntityFromMemory(link.entity_id, updated.id, d);
1601
- }
1602
- runEntityExtraction(updated, existing.project_id || undefined, d);
1603
- }
1604
- } catch {}
1605
- return updated;
1606
- }
1607
- function deleteMemory(id, db) {
1608
- const d = db || getDatabase();
1609
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
1610
- return result.changes > 0;
1611
- }
1612
- function bulkDeleteMemories(ids, db) {
1613
- const d = db || getDatabase();
1614
- if (ids.length === 0)
1615
- return 0;
1616
- const placeholders = ids.map(() => "?").join(",");
1617
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
1618
- const count = countRow.c;
1619
- if (count > 0) {
1620
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
1621
- }
1622
- return count;
1623
- }
1624
- function touchMemory(id, db) {
1625
- const d = db || getDatabase();
1626
- d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
1549
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1550
+ return getEntity(id, d);
1627
1551
  }
1628
- function cleanExpiredMemories(db) {
1552
+ function deleteEntity(id, db) {
1629
1553
  const d = db || getDatabase();
1630
- const timestamp = now();
1631
- const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1632
- const count = countRow.c;
1633
- if (count > 0) {
1634
- d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1635
- }
1636
- return count;
1554
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1555
+ if (result.changes === 0)
1556
+ throw new EntityNotFoundError(id);
1637
1557
  }
1638
- function getMemoryVersions(memoryId, db) {
1558
+ function mergeEntities(sourceId, targetId, db) {
1639
1559
  const d = db || getDatabase();
1640
- try {
1641
- const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1642
- return rows.map((row) => ({
1643
- id: row["id"],
1644
- memory_id: row["memory_id"],
1645
- version: row["version"],
1646
- value: row["value"],
1647
- importance: row["importance"],
1648
- scope: row["scope"],
1649
- category: row["category"],
1650
- tags: JSON.parse(row["tags"] || "[]"),
1651
- summary: row["summary"] || null,
1652
- pinned: !!row["pinned"],
1653
- status: row["status"],
1654
- created_at: row["created_at"]
1655
- }));
1656
- } catch {
1657
- return [];
1658
- }
1560
+ getEntity(sourceId, d);
1561
+ getEntity(targetId, d);
1562
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
1563
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
1564
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
1565
+ sourceId,
1566
+ sourceId
1567
+ ]);
1568
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
1569
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
1570
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
1571
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
1572
+ return getEntity(targetId, d);
1659
1573
  }
1660
- // src/db/locks.ts
1661
- function parseLockRow(row) {
1574
+
1575
+ // src/lib/search.ts
1576
+ function parseMemoryRow2(row) {
1662
1577
  return {
1663
1578
  id: row["id"],
1664
- resource_type: row["resource_type"],
1665
- resource_id: row["resource_id"],
1666
- agent_id: row["agent_id"],
1667
- lock_type: row["lock_type"],
1668
- locked_at: row["locked_at"],
1669
- expires_at: row["expires_at"]
1670
- };
1671
- }
1672
- function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
1673
- const d = db || getDatabase();
1674
- cleanExpiredLocks(d);
1675
- const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId, lockType);
1676
- if (ownLock) {
1677
- const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1678
- d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
1679
- newExpiry,
1680
- ownLock["id"]
1681
- ]);
1682
- return parseLockRow({ ...ownLock, expires_at: newExpiry });
1683
- }
1684
- if (lockType === "exclusive") {
1685
- const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > datetime('now')").get(resourceType, resourceId, agentId);
1686
- if (existing) {
1687
- return null;
1688
- }
1689
- }
1690
- const id = shortUuid();
1691
- const lockedAt = now();
1692
- const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1693
- d.run("INSERT INTO resource_locks (id, resource_type, resource_id, agent_id, lock_type, locked_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, resourceType, resourceId, agentId, lockType, lockedAt, expiresAt]);
1694
- return {
1695
- id,
1696
- resource_type: resourceType,
1697
- resource_id: resourceId,
1698
- agent_id: agentId,
1699
- lock_type: lockType,
1700
- locked_at: lockedAt,
1701
- expires_at: expiresAt
1579
+ key: row["key"],
1580
+ value: row["value"],
1581
+ category: row["category"],
1582
+ scope: row["scope"],
1583
+ summary: row["summary"] || null,
1584
+ tags: JSON.parse(row["tags"] || "[]"),
1585
+ importance: row["importance"],
1586
+ source: row["source"],
1587
+ status: row["status"],
1588
+ pinned: !!row["pinned"],
1589
+ agent_id: row["agent_id"] || null,
1590
+ project_id: row["project_id"] || null,
1591
+ session_id: row["session_id"] || null,
1592
+ metadata: JSON.parse(row["metadata"] || "{}"),
1593
+ access_count: row["access_count"],
1594
+ version: row["version"],
1595
+ expires_at: row["expires_at"] || null,
1596
+ created_at: row["created_at"],
1597
+ updated_at: row["updated_at"],
1598
+ accessed_at: row["accessed_at"] || null
1702
1599
  };
1703
1600
  }
1704
- function releaseLock(lockId, agentId, db) {
1705
- const d = db || getDatabase();
1706
- const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1707
- return result.changes > 0;
1708
- }
1709
- function releaseResourceLocks(agentId, resourceType, resourceId, db) {
1710
- const d = db || getDatabase();
1711
- const result = d.run("DELETE FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ?", [agentId, resourceType, resourceId]);
1712
- return result.changes;
1713
- }
1714
- function releaseAllAgentLocks(agentId, db) {
1715
- const d = db || getDatabase();
1716
- const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1717
- return result.changes;
1601
+ function preprocessQuery(query) {
1602
+ let q = query.trim();
1603
+ q = q.replace(/\s+/g, " ");
1604
+ q = q.normalize("NFC");
1605
+ return q;
1718
1606
  }
1719
- function checkLock(resourceType, resourceId, lockType, db) {
1720
- const d = db || getDatabase();
1721
- cleanExpiredLocks(d);
1722
- const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1723
- const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
1724
- return rows.map(parseLockRow);
1725
- }
1726
- function agentHoldsLock(agentId, resourceType, resourceId, lockType, db) {
1727
- const d = db || getDatabase();
1728
- const query = lockType ? "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > datetime('now')" : "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND expires_at > datetime('now')";
1729
- const row = lockType ? d.query(query).get(agentId, resourceType, resourceId, lockType) : d.query(query).get(agentId, resourceType, resourceId);
1730
- return row ? parseLockRow(row) : null;
1731
- }
1732
- function listAgentLocks(agentId, db) {
1733
- const d = db || getDatabase();
1734
- cleanExpiredLocks(d);
1735
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
1736
- return rows.map(parseLockRow);
1737
- }
1738
- function cleanExpiredLocks(db) {
1739
- const d = db || getDatabase();
1740
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
1741
- return result.changes;
1742
- }
1743
- // src/lib/memory-lock.ts
1744
- var MEMORY_WRITE_TTL = 30;
1745
- function memoryLockId(key, scope, projectId) {
1746
- return `${scope}:${key}:${projectId ?? ""}`;
1747
- }
1748
- function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
1749
- const d = db || getDatabase();
1750
- return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
1751
- }
1752
- function releaseMemoryWriteLock(lockId, agentId, db) {
1753
- const d = db || getDatabase();
1754
- return releaseLock(lockId, agentId, d);
1755
- }
1756
- function checkMemoryWriteLock(key, scope, projectId, db) {
1757
- const d = db || getDatabase();
1758
- const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
1759
- return locks[0] ?? null;
1760
- }
1761
- function withMemoryLock(agentId, key, scope, projectId, fn, ttlSeconds = MEMORY_WRITE_TTL, db) {
1762
- const d = db || getDatabase();
1763
- const lock = acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds, d);
1764
- if (!lock) {
1765
- const existing = checkMemoryWriteLock(key, scope, projectId, d);
1766
- throw new MemoryLockConflictError(key, scope, existing?.agent_id ?? "unknown");
1767
- }
1768
- try {
1769
- return fn();
1770
- } finally {
1771
- releaseLock(lock.id, agentId, d);
1772
- }
1773
- }
1774
-
1775
- class MemoryLockConflictError extends Error {
1776
- conflict = true;
1777
- key;
1778
- scope;
1779
- blocking_agent_id;
1780
- constructor(key, scope, blockingAgentId) {
1781
- super(`Memory key "${key}" (scope: ${scope}) is currently write-locked by agent ${blockingAgentId}. ` + "Retry after a few seconds or use optimistic locking (version field).");
1782
- this.name = "MemoryLockConflictError";
1783
- this.key = key;
1784
- this.scope = scope;
1785
- this.blocking_agent_id = blockingAgentId;
1786
- }
1787
- }
1788
- // src/lib/search.ts
1789
- function parseMemoryRow2(row) {
1790
- return {
1791
- id: row["id"],
1792
- key: row["key"],
1793
- value: row["value"],
1794
- category: row["category"],
1795
- scope: row["scope"],
1796
- summary: row["summary"] || null,
1797
- tags: JSON.parse(row["tags"] || "[]"),
1798
- importance: row["importance"],
1799
- source: row["source"],
1800
- status: row["status"],
1801
- pinned: !!row["pinned"],
1802
- agent_id: row["agent_id"] || null,
1803
- project_id: row["project_id"] || null,
1804
- session_id: row["session_id"] || null,
1805
- metadata: JSON.parse(row["metadata"] || "{}"),
1806
- access_count: row["access_count"],
1807
- version: row["version"],
1808
- expires_at: row["expires_at"] || null,
1809
- created_at: row["created_at"],
1810
- updated_at: row["updated_at"],
1811
- accessed_at: row["accessed_at"] || null
1812
- };
1813
- }
1814
- function preprocessQuery(query) {
1815
- let q = query.trim();
1816
- q = q.replace(/\s+/g, " ");
1817
- q = q.normalize("NFC");
1818
- return q;
1819
- }
1820
- function escapeLikePattern(s) {
1821
- return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1607
+ function escapeLikePattern(s) {
1608
+ return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1822
1609
  }
1823
1610
  var STOP_WORDS = new Set([
1824
1611
  "a",
@@ -2309,6 +2096,12 @@ function searchMemories(query, filter, db) {
2309
2096
  const offset = filter?.offset ?? 0;
2310
2097
  const limit = filter?.limit ?? scored.length;
2311
2098
  const finalResults = scored.slice(offset, offset + limit);
2099
+ if (finalResults.length > 0 && scored.length > 0) {
2100
+ const topScore = scored[0]?.score ?? 0;
2101
+ const secondScore = scored[1]?.score ?? 0;
2102
+ const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
2103
+ finalResults[0] = { ...finalResults[0], confidence };
2104
+ }
2312
2105
  logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
2313
2106
  return finalResults;
2314
2107
  }
@@ -2319,6 +2112,148 @@ function logSearchQuery(query, resultCount, agentId, projectId, db) {
2319
2112
  d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
2320
2113
  } catch {}
2321
2114
  }
2115
+ // src/lib/config.ts
2116
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
2117
+ import { homedir } from "os";
2118
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
2119
+ var DEFAULT_CONFIG = {
2120
+ default_scope: "private",
2121
+ default_category: "knowledge",
2122
+ default_importance: 5,
2123
+ max_entries: 1000,
2124
+ max_entries_per_scope: {
2125
+ global: 500,
2126
+ shared: 300,
2127
+ private: 200
2128
+ },
2129
+ injection: {
2130
+ max_tokens: 500,
2131
+ min_importance: 5,
2132
+ categories: ["preference", "fact"],
2133
+ refresh_interval: 5
2134
+ },
2135
+ extraction: {
2136
+ enabled: true,
2137
+ min_confidence: 0.5
2138
+ },
2139
+ sync_agents: ["claude", "codex", "gemini"],
2140
+ auto_cleanup: {
2141
+ enabled: true,
2142
+ expired_check_interval: 3600,
2143
+ unused_archive_days: 7,
2144
+ stale_deprioritize_days: 14
2145
+ }
2146
+ };
2147
+ function deepMerge(target, source) {
2148
+ const result = { ...target };
2149
+ for (const key of Object.keys(source)) {
2150
+ const sourceVal = source[key];
2151
+ const targetVal = result[key];
2152
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
2153
+ result[key] = deepMerge(targetVal, sourceVal);
2154
+ } else {
2155
+ result[key] = sourceVal;
2156
+ }
2157
+ }
2158
+ return result;
2159
+ }
2160
+ var VALID_SCOPES = ["global", "shared", "private"];
2161
+ var VALID_CATEGORIES = [
2162
+ "preference",
2163
+ "fact",
2164
+ "knowledge",
2165
+ "history"
2166
+ ];
2167
+ function isValidScope(value) {
2168
+ return VALID_SCOPES.includes(value);
2169
+ }
2170
+ function isValidCategory(value) {
2171
+ return VALID_CATEGORIES.includes(value);
2172
+ }
2173
+ function loadConfig() {
2174
+ const configPath = join2(homedir(), ".mementos", "config.json");
2175
+ let fileConfig = {};
2176
+ if (existsSync2(configPath)) {
2177
+ try {
2178
+ const raw = readFileSync(configPath, "utf-8");
2179
+ fileConfig = JSON.parse(raw);
2180
+ } catch {}
2181
+ }
2182
+ const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
2183
+ const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
2184
+ if (envScope && isValidScope(envScope)) {
2185
+ merged.default_scope = envScope;
2186
+ }
2187
+ const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
2188
+ if (envCategory && isValidCategory(envCategory)) {
2189
+ merged.default_category = envCategory;
2190
+ }
2191
+ const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
2192
+ if (envImportance) {
2193
+ const parsed = parseInt(envImportance, 10);
2194
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
2195
+ merged.default_importance = parsed;
2196
+ }
2197
+ }
2198
+ return merged;
2199
+ }
2200
+ function profilesDir() {
2201
+ return join2(homedir(), ".mementos", "profiles");
2202
+ }
2203
+ function globalConfigPath() {
2204
+ return join2(homedir(), ".mementos", "config.json");
2205
+ }
2206
+ function readGlobalConfig() {
2207
+ const p = globalConfigPath();
2208
+ if (!existsSync2(p))
2209
+ return {};
2210
+ try {
2211
+ return JSON.parse(readFileSync(p, "utf-8"));
2212
+ } catch {
2213
+ return {};
2214
+ }
2215
+ }
2216
+ function writeGlobalConfig(data) {
2217
+ const p = globalConfigPath();
2218
+ ensureDir2(dirname2(p));
2219
+ writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
2220
+ }
2221
+ function getActiveProfile() {
2222
+ const envProfile = process.env["MEMENTOS_PROFILE"];
2223
+ if (envProfile)
2224
+ return envProfile.trim();
2225
+ const cfg = readGlobalConfig();
2226
+ return cfg["active_profile"] || null;
2227
+ }
2228
+ function setActiveProfile(name) {
2229
+ const cfg = readGlobalConfig();
2230
+ if (name === null) {
2231
+ delete cfg["active_profile"];
2232
+ } else {
2233
+ cfg["active_profile"] = name;
2234
+ }
2235
+ writeGlobalConfig(cfg);
2236
+ }
2237
+ function listProfiles() {
2238
+ const dir = profilesDir();
2239
+ if (!existsSync2(dir))
2240
+ return [];
2241
+ return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
2242
+ }
2243
+ function deleteProfile(name) {
2244
+ const dbPath = join2(profilesDir(), `${name}.db`);
2245
+ if (!existsSync2(dbPath))
2246
+ return false;
2247
+ unlinkSync(dbPath);
2248
+ if (getActiveProfile() === name)
2249
+ setActiveProfile(null);
2250
+ return true;
2251
+ }
2252
+ function ensureDir2(dir) {
2253
+ if (!existsSync2(dir)) {
2254
+ mkdirSync2(dir, { recursive: true });
2255
+ }
2256
+ }
2322
2257
  // src/lib/injector.ts
2323
2258
  class MemoryInjector {
2324
2259
  config;
@@ -2486,183 +2421,1092 @@ function enforceQuotas(config, db) {
2486
2421
  d.run(`DELETE FROM memories WHERE id IN (${subquery})`, [scope, excess]);
2487
2422
  totalEvicted += delCount;
2488
2423
  }
2489
- return totalEvicted;
2424
+ return totalEvicted;
2425
+ }
2426
+ function archiveStale(staleDays, db) {
2427
+ const d = db || getDatabase();
2428
+ const timestamp = now();
2429
+ const cutoff = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000).toISOString();
2430
+ const archiveWhere = `status = 'active' AND pinned = 0 AND COALESCE(accessed_at, created_at) < ?`;
2431
+ const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${archiveWhere}`).get(cutoff).c;
2432
+ if (count > 0) {
2433
+ d.run(`UPDATE memories SET status = 'archived', updated_at = ? WHERE ${archiveWhere}`, [timestamp, cutoff]);
2434
+ }
2435
+ return count;
2436
+ }
2437
+ function archiveUnused(days, db) {
2438
+ const d = db || getDatabase();
2439
+ const timestamp = now();
2440
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2441
+ const unusedWhere = `status = 'active' AND pinned = 0 AND access_count = 0 AND created_at < ?`;
2442
+ const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${unusedWhere}`).get(cutoff).c;
2443
+ if (count > 0) {
2444
+ d.run(`UPDATE memories SET status = 'archived', updated_at = ? WHERE ${unusedWhere}`, [timestamp, cutoff]);
2445
+ }
2446
+ return count;
2447
+ }
2448
+ function deprioritizeStale(days, db) {
2449
+ const d = db || getDatabase();
2450
+ const timestamp = now();
2451
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2452
+ const deprioWhere = `status = 'active' AND pinned = 0 AND importance > 1 AND COALESCE(accessed_at, updated_at) < ?`;
2453
+ const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${deprioWhere}`).get(cutoff).c;
2454
+ if (count > 0) {
2455
+ d.run(`UPDATE memories
2456
+ SET importance = MAX(importance - 1, 1),
2457
+ version = version + 1,
2458
+ updated_at = ?
2459
+ WHERE ${deprioWhere}`, [timestamp, cutoff]);
2460
+ }
2461
+ return count;
2462
+ }
2463
+ function runCleanup(config, db) {
2464
+ const d = db || getDatabase();
2465
+ const expired = cleanExpiredMemories(d);
2466
+ const evicted = enforceQuotas(config, d);
2467
+ const archived = archiveStale(90, d);
2468
+ const unused_archived = archiveUnused(config.auto_cleanup.unused_archive_days ?? 7, d);
2469
+ const deprioritized = deprioritizeStale(config.auto_cleanup.stale_deprioritize_days ?? 14, d);
2470
+ return { expired, evicted, archived, unused_archived, deprioritized };
2471
+ }
2472
+ // src/lib/sync.ts
2473
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2474
+ import { homedir as homedir2 } from "os";
2475
+ import { join as join3 } from "path";
2476
+ function getAgentSyncDir(agentName) {
2477
+ const dir = join3(homedir2(), ".mementos", "agents", agentName);
2478
+ if (!existsSync3(dir)) {
2479
+ mkdirSync3(dir, { recursive: true });
2480
+ }
2481
+ return dir;
2482
+ }
2483
+ function setHighWaterMark(agentDir, timestamp) {
2484
+ const markFile = join3(agentDir, ".highwatermark");
2485
+ writeFileSync2(markFile, timestamp, "utf-8");
2486
+ }
2487
+ function resolveConflict(local, remote, resolution) {
2488
+ switch (resolution) {
2489
+ case "prefer-local":
2490
+ return "local";
2491
+ case "prefer-remote":
2492
+ return "remote";
2493
+ case "prefer-newer":
2494
+ return new Date(local.updated_at).getTime() >= new Date(remote.updated_at).getTime() ? "local" : "remote";
2495
+ }
2496
+ }
2497
+ function pushMemories(agentName, agentId, projectId, db) {
2498
+ const agentDir = getAgentSyncDir(agentName);
2499
+ const memories = listMemories({
2500
+ agent_id: agentId,
2501
+ project_id: projectId,
2502
+ status: "active",
2503
+ limit: 1e4
2504
+ }, db);
2505
+ const outFile = join3(agentDir, "memories.json");
2506
+ writeFileSync2(outFile, JSON.stringify(memories, null, 2), "utf-8");
2507
+ if (memories.length > 0) {
2508
+ const latest = memories.reduce((a, b) => new Date(a.updated_at).getTime() > new Date(b.updated_at).getTime() ? a : b);
2509
+ setHighWaterMark(agentDir, latest.updated_at);
2510
+ }
2511
+ return memories.length;
2512
+ }
2513
+ function pullMemories(agentName, conflictResolution = "prefer-newer", db) {
2514
+ const agentDir = getAgentSyncDir(agentName);
2515
+ const inFile = join3(agentDir, "memories.json");
2516
+ if (!existsSync3(inFile)) {
2517
+ return { pulled: 0, conflicts: 0 };
2518
+ }
2519
+ const raw = readFileSync2(inFile, "utf-8");
2520
+ let remoteMemories;
2521
+ try {
2522
+ remoteMemories = JSON.parse(raw);
2523
+ } catch {
2524
+ return { pulled: 0, conflicts: 0 };
2525
+ }
2526
+ let pulled = 0;
2527
+ let conflicts = 0;
2528
+ for (const remote of remoteMemories) {
2529
+ const localMemories = listMemories({
2530
+ search: remote.key,
2531
+ scope: remote.scope,
2532
+ agent_id: remote.agent_id || undefined,
2533
+ project_id: remote.project_id || undefined,
2534
+ limit: 1
2535
+ }, db);
2536
+ const local = localMemories.find((m) => m.key === remote.key);
2537
+ if (local) {
2538
+ const winner = resolveConflict(local, remote, conflictResolution);
2539
+ if (winner === "remote") {
2540
+ createMemory({
2541
+ key: remote.key,
2542
+ value: remote.value,
2543
+ category: remote.category,
2544
+ scope: remote.scope,
2545
+ summary: remote.summary || undefined,
2546
+ tags: remote.tags,
2547
+ importance: remote.importance,
2548
+ source: remote.source,
2549
+ agent_id: remote.agent_id || undefined,
2550
+ project_id: remote.project_id || undefined,
2551
+ session_id: remote.session_id || undefined,
2552
+ metadata: remote.metadata,
2553
+ expires_at: remote.expires_at || undefined
2554
+ }, "merge", db);
2555
+ pulled++;
2556
+ }
2557
+ conflicts++;
2558
+ } else {
2559
+ createMemory({
2560
+ key: remote.key,
2561
+ value: remote.value,
2562
+ category: remote.category,
2563
+ scope: remote.scope,
2564
+ summary: remote.summary || undefined,
2565
+ tags: remote.tags,
2566
+ importance: remote.importance,
2567
+ source: remote.source,
2568
+ agent_id: remote.agent_id || undefined,
2569
+ project_id: remote.project_id || undefined,
2570
+ session_id: remote.session_id || undefined,
2571
+ metadata: remote.metadata,
2572
+ expires_at: remote.expires_at || undefined
2573
+ }, "create", db);
2574
+ pulled++;
2575
+ }
2576
+ }
2577
+ return { pulled, conflicts };
2578
+ }
2579
+ function syncMemories(agentName, direction = "both", options = {}) {
2580
+ const result = {
2581
+ pushed: 0,
2582
+ pulled: 0,
2583
+ conflicts: 0,
2584
+ errors: []
2585
+ };
2586
+ try {
2587
+ if (direction === "push" || direction === "both") {
2588
+ result.pushed = pushMemories(agentName, options.agent_id, options.project_id, options.db);
2589
+ }
2590
+ if (direction === "pull" || direction === "both") {
2591
+ const pullResult = pullMemories(agentName, options.conflict_resolution || "prefer-newer", options.db);
2592
+ result.pulled = pullResult.pulled;
2593
+ result.conflicts = pullResult.conflicts;
2594
+ }
2595
+ } catch (e) {
2596
+ result.errors.push(e instanceof Error ? e.message : String(e));
2597
+ }
2598
+ return result;
2599
+ }
2600
+ var defaultSyncAgents = ["claude", "codex", "gemini"];
2601
+ // src/db/relations.ts
2602
+ function parseRelationRow(row) {
2603
+ return {
2604
+ id: row["id"],
2605
+ source_entity_id: row["source_entity_id"],
2606
+ target_entity_id: row["target_entity_id"],
2607
+ relation_type: row["relation_type"],
2608
+ weight: row["weight"],
2609
+ metadata: JSON.parse(row["metadata"] || "{}"),
2610
+ created_at: row["created_at"]
2611
+ };
2612
+ }
2613
+ function parseEntityRow3(row) {
2614
+ return {
2615
+ id: row["id"],
2616
+ name: row["name"],
2617
+ type: row["type"],
2618
+ description: row["description"] || null,
2619
+ metadata: JSON.parse(row["metadata"] || "{}"),
2620
+ project_id: row["project_id"] || null,
2621
+ created_at: row["created_at"],
2622
+ updated_at: row["updated_at"]
2623
+ };
2624
+ }
2625
+ function createRelation(input, db) {
2626
+ const d = db || getDatabase();
2627
+ const id = shortUuid();
2628
+ const timestamp = now();
2629
+ const weight = input.weight ?? 1;
2630
+ const metadata = JSON.stringify(input.metadata ?? {});
2631
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
2632
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2633
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
2634
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
2635
+ const row = d.query(`SELECT * FROM relations
2636
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
2637
+ const relation = parseRelationRow(row);
2638
+ hookRegistry.runHooks("PostRelationCreate", {
2639
+ relationId: relation.id,
2640
+ sourceEntityId: relation.source_entity_id,
2641
+ targetEntityId: relation.target_entity_id,
2642
+ relationType: relation.relation_type,
2643
+ timestamp: Date.now()
2644
+ });
2645
+ return relation;
2646
+ }
2647
+ function getRelation(id, db) {
2648
+ const d = db || getDatabase();
2649
+ const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
2650
+ if (!row)
2651
+ throw new Error(`Relation not found: ${id}`);
2652
+ return parseRelationRow(row);
2653
+ }
2654
+ function listRelations(filter, db) {
2655
+ const d = db || getDatabase();
2656
+ const conditions = [];
2657
+ const params = [];
2658
+ if (filter.entity_id) {
2659
+ const dir = filter.direction || "both";
2660
+ if (dir === "outgoing") {
2661
+ conditions.push("source_entity_id = ?");
2662
+ params.push(filter.entity_id);
2663
+ } else if (dir === "incoming") {
2664
+ conditions.push("target_entity_id = ?");
2665
+ params.push(filter.entity_id);
2666
+ } else {
2667
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
2668
+ params.push(filter.entity_id, filter.entity_id);
2669
+ }
2670
+ }
2671
+ if (filter.relation_type) {
2672
+ conditions.push("relation_type = ?");
2673
+ params.push(filter.relation_type);
2674
+ }
2675
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2676
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
2677
+ return rows.map(parseRelationRow);
2678
+ }
2679
+ function deleteRelation(id, db) {
2680
+ const d = db || getDatabase();
2681
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
2682
+ if (result.changes === 0)
2683
+ throw new Error(`Relation not found: ${id}`);
2684
+ }
2685
+ function getRelatedEntities(entityId, relationType, db) {
2686
+ const d = db || getDatabase();
2687
+ let sql;
2688
+ const params = [];
2689
+ if (relationType) {
2690
+ sql = `
2691
+ SELECT DISTINCT e.* FROM entities e
2692
+ JOIN relations r ON (
2693
+ (r.source_entity_id = ? AND r.target_entity_id = e.id)
2694
+ OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
2695
+ )
2696
+ WHERE r.relation_type = ?
2697
+ `;
2698
+ params.push(entityId, entityId, relationType);
2699
+ } else {
2700
+ sql = `
2701
+ SELECT DISTINCT e.* FROM entities e
2702
+ JOIN relations r ON (
2703
+ (r.source_entity_id = ? AND r.target_entity_id = e.id)
2704
+ OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
2705
+ )
2706
+ `;
2707
+ params.push(entityId, entityId);
2708
+ }
2709
+ const rows = d.query(sql).all(...params);
2710
+ return rows.map(parseEntityRow3);
2711
+ }
2712
+ function getEntityGraph(entityId, depth = 2, db) {
2713
+ const d = db || getDatabase();
2714
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
2715
+ VALUES(?, 0)
2716
+ UNION
2717
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
2718
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
2719
+ WHERE g.depth < ?
2720
+ )
2721
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
2722
+ const entities = entityRows.map(parseEntityRow3);
2723
+ const entityIds = new Set(entities.map((e) => e.id));
2724
+ if (entityIds.size === 0) {
2725
+ return { entities: [], relations: [] };
2726
+ }
2727
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
2728
+ const relationRows = d.query(`SELECT * FROM relations
2729
+ WHERE source_entity_id IN (${placeholders})
2730
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
2731
+ const relations = relationRows.map(parseRelationRow);
2732
+ return { entities, relations };
2733
+ }
2734
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
2735
+ const d = db || getDatabase();
2736
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
2737
+ SELECT ?, ?, 0
2738
+ UNION
2739
+ SELECT
2740
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
2741
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
2742
+ p.depth + 1
2743
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
2744
+ WHERE p.depth < ?
2745
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
2746
+ )
2747
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
2748
+ if (!rows)
2749
+ return null;
2750
+ const ids = rows.trail.split(",");
2751
+ const entities = [];
2752
+ for (const id of ids) {
2753
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
2754
+ if (row)
2755
+ entities.push(parseEntityRow3(row));
2756
+ }
2757
+ return entities.length > 0 ? entities : null;
2758
+ }
2759
+ // src/lib/providers/base.ts
2760
+ var DEFAULT_AUTO_MEMORY_CONFIG = {
2761
+ provider: "anthropic",
2762
+ model: "claude-haiku-4-5",
2763
+ enabled: true,
2764
+ minImportance: 4,
2765
+ autoEntityLink: true,
2766
+ fallback: ["cerebras", "openai"]
2767
+ };
2768
+
2769
+ class BaseProvider {
2770
+ config;
2771
+ constructor(config) {
2772
+ this.config = config;
2773
+ }
2774
+ parseJSON(raw) {
2775
+ try {
2776
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
2777
+ return JSON.parse(cleaned);
2778
+ } catch {
2779
+ return null;
2780
+ }
2781
+ }
2782
+ clampImportance(value) {
2783
+ const n = Number(value);
2784
+ if (isNaN(n))
2785
+ return 5;
2786
+ return Math.max(0, Math.min(10, Math.round(n)));
2787
+ }
2788
+ normaliseMemory(raw) {
2789
+ if (!raw || typeof raw !== "object")
2790
+ return null;
2791
+ const m = raw;
2792
+ if (typeof m.content !== "string" || !m.content.trim())
2793
+ return null;
2794
+ const validScopes = ["private", "shared", "global"];
2795
+ const validCategories = [
2796
+ "preference",
2797
+ "fact",
2798
+ "knowledge",
2799
+ "history"
2800
+ ];
2801
+ return {
2802
+ content: m.content.trim(),
2803
+ category: validCategories.includes(m.category) ? m.category : "knowledge",
2804
+ importance: this.clampImportance(m.importance),
2805
+ tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
2806
+ suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
2807
+ reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
2808
+ };
2809
+ }
2810
+ }
2811
+ var MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
2812
+ Given text, extract facts worth remembering as structured JSON.
2813
+ Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
2814
+ Ignore: greetings, filler, questions without answers, temporary states.
2815
+ Output ONLY a JSON array \u2014 no markdown, no explanation.`;
2816
+ var MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
2817
+ ${context.projectName ? `Project: ${context.projectName}` : ""}
2818
+ ${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
2819
+ ${context.existingMemoriesSummary}` : ""}
2820
+
2821
+ Text:
2822
+ ${text}
2823
+
2824
+ Return a JSON array of objects with these exact fields:
2825
+ - content: string (the memory, concise and specific)
2826
+ - category: "preference" | "fact" | "knowledge" | "history"
2827
+ - importance: number 0-10 (10 = critical, 0 = trivial)
2828
+ - tags: string[] (lowercase keywords)
2829
+ - suggestedScope: "private" | "shared" | "global"
2830
+ - reasoning: string (one sentence why this is worth remembering)
2831
+
2832
+ Return [] if nothing is worth remembering.`;
2833
+ var ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
2834
+ Given text, identify named entities and their relationships.
2835
+ Output ONLY valid JSON \u2014 no markdown, no explanation.`;
2836
+ var ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
2837
+
2838
+ Text: ${text}
2839
+
2840
+ Return JSON with this exact shape:
2841
+ {
2842
+ "entities": [
2843
+ { "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
2844
+ ],
2845
+ "relations": [
2846
+ { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
2847
+ ]
2848
+ }`;
2849
+
2850
+ // src/lib/providers/anthropic.ts
2851
+ var ANTHROPIC_MODELS = {
2852
+ default: "claude-haiku-4-5",
2853
+ premium: "claude-sonnet-4-5"
2854
+ };
2855
+
2856
+ class AnthropicProvider extends BaseProvider {
2857
+ name = "anthropic";
2858
+ baseUrl = "https://api.anthropic.com/v1";
2859
+ constructor(config) {
2860
+ const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
2861
+ super({
2862
+ apiKey,
2863
+ model: config?.model ?? ANTHROPIC_MODELS.default,
2864
+ maxTokens: config?.maxTokens ?? 1024,
2865
+ temperature: config?.temperature ?? 0,
2866
+ timeoutMs: config?.timeoutMs ?? 15000
2867
+ });
2868
+ }
2869
+ async extractMemories(text, context) {
2870
+ if (!this.config.apiKey)
2871
+ return [];
2872
+ try {
2873
+ const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2874
+ const parsed = this.parseJSON(response);
2875
+ if (!Array.isArray(parsed))
2876
+ return [];
2877
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2878
+ } catch (err) {
2879
+ console.error("[anthropic] extractMemories failed:", err);
2880
+ return [];
2881
+ }
2882
+ }
2883
+ async extractEntities(text) {
2884
+ const empty = { entities: [], relations: [] };
2885
+ if (!this.config.apiKey)
2886
+ return empty;
2887
+ try {
2888
+ const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2889
+ const parsed = this.parseJSON(response);
2890
+ if (!parsed || typeof parsed !== "object")
2891
+ return empty;
2892
+ return {
2893
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2894
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2895
+ };
2896
+ } catch (err) {
2897
+ console.error("[anthropic] extractEntities failed:", err);
2898
+ return empty;
2899
+ }
2900
+ }
2901
+ async scoreImportance(content, _context) {
2902
+ if (!this.config.apiKey)
2903
+ return 5;
2904
+ try {
2905
+ const response = await this.callAPI("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2906
+
2907
+ "${content}"
2908
+
2909
+ Return only a number 0-10.`);
2910
+ return this.clampImportance(response.trim());
2911
+ } catch {
2912
+ return 5;
2913
+ }
2914
+ }
2915
+ async callAPI(systemPrompt, userMessage) {
2916
+ const controller = new AbortController;
2917
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2918
+ try {
2919
+ const res = await fetch(`${this.baseUrl}/messages`, {
2920
+ method: "POST",
2921
+ headers: {
2922
+ "Content-Type": "application/json",
2923
+ "x-api-key": this.config.apiKey,
2924
+ "anthropic-version": "2023-06-01"
2925
+ },
2926
+ body: JSON.stringify({
2927
+ model: this.config.model,
2928
+ max_tokens: this.config.maxTokens ?? 1024,
2929
+ temperature: this.config.temperature ?? 0,
2930
+ system: systemPrompt,
2931
+ messages: [{ role: "user", content: userMessage }]
2932
+ }),
2933
+ signal: controller.signal
2934
+ });
2935
+ if (!res.ok) {
2936
+ const body = await res.text().catch(() => "");
2937
+ throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
2938
+ }
2939
+ const data = await res.json();
2940
+ return data.content?.[0]?.text ?? "";
2941
+ } finally {
2942
+ clearTimeout(timeout);
2943
+ }
2944
+ }
2945
+ }
2946
+
2947
+ // src/lib/providers/openai-compat.ts
2948
+ class OpenAICompatProvider extends BaseProvider {
2949
+ constructor(config) {
2950
+ super(config);
2951
+ }
2952
+ async extractMemories(text, context) {
2953
+ if (!this.config.apiKey)
2954
+ return [];
2955
+ try {
2956
+ const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2957
+ const parsed = this.parseJSON(response);
2958
+ if (!Array.isArray(parsed))
2959
+ return [];
2960
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2961
+ } catch (err) {
2962
+ console.error(`[${this.name}] extractMemories failed:`, err);
2963
+ return [];
2964
+ }
2965
+ }
2966
+ async extractEntities(text) {
2967
+ const empty = { entities: [], relations: [] };
2968
+ if (!this.config.apiKey)
2969
+ return empty;
2970
+ try {
2971
+ const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2972
+ const parsed = this.parseJSON(response);
2973
+ if (!parsed || typeof parsed !== "object")
2974
+ return empty;
2975
+ return {
2976
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2977
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2978
+ };
2979
+ } catch (err) {
2980
+ console.error(`[${this.name}] extractEntities failed:`, err);
2981
+ return empty;
2982
+ }
2983
+ }
2984
+ async scoreImportance(content, _context) {
2985
+ if (!this.config.apiKey)
2986
+ return 5;
2987
+ try {
2988
+ const response = await this.callWithRetry("You are an importance scorer. Return only a single integer 0-10. No explanation.", `How important is this memory for an AI agent to retain long-term?
2989
+
2990
+ "${content}"
2991
+
2992
+ Return only a number 0-10.`);
2993
+ return this.clampImportance(response.trim());
2994
+ } catch {
2995
+ return 5;
2996
+ }
2997
+ }
2998
+ async callWithRetry(systemPrompt, userMessage, retries = 3) {
2999
+ let lastError = null;
3000
+ for (let attempt = 0;attempt < retries; attempt++) {
3001
+ try {
3002
+ return await this.callAPI(systemPrompt, userMessage);
3003
+ } catch (err) {
3004
+ lastError = err instanceof Error ? err : new Error(String(err));
3005
+ const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
3006
+ if (!isRateLimit || attempt === retries - 1)
3007
+ throw lastError;
3008
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
3009
+ }
3010
+ }
3011
+ throw lastError ?? new Error("Unknown error");
3012
+ }
3013
+ async callAPI(systemPrompt, userMessage) {
3014
+ const controller = new AbortController;
3015
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
3016
+ try {
3017
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
3018
+ method: "POST",
3019
+ headers: {
3020
+ "Content-Type": "application/json",
3021
+ [this.authHeader]: `Bearer ${this.config.apiKey}`
3022
+ },
3023
+ body: JSON.stringify({
3024
+ model: this.config.model,
3025
+ max_tokens: this.config.maxTokens ?? 1024,
3026
+ temperature: this.config.temperature ?? 0,
3027
+ messages: [
3028
+ { role: "system", content: systemPrompt },
3029
+ { role: "user", content: userMessage }
3030
+ ]
3031
+ }),
3032
+ signal: controller.signal
3033
+ });
3034
+ if (!res.ok) {
3035
+ const body = await res.text().catch(() => "");
3036
+ throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
3037
+ }
3038
+ const data = await res.json();
3039
+ return data.choices?.[0]?.message?.content ?? "";
3040
+ } finally {
3041
+ clearTimeout(timeout);
3042
+ }
3043
+ }
2490
3044
  }
2491
- function archiveStale(staleDays, db) {
2492
- const d = db || getDatabase();
2493
- const timestamp = now();
2494
- const cutoff = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000).toISOString();
2495
- const archiveWhere = `status = 'active' AND pinned = 0 AND COALESCE(accessed_at, created_at) < ?`;
2496
- const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${archiveWhere}`).get(cutoff).c;
2497
- if (count > 0) {
2498
- d.run(`UPDATE memories SET status = 'archived', updated_at = ? WHERE ${archiveWhere}`, [timestamp, cutoff]);
3045
+
3046
+ // src/lib/providers/openai.ts
3047
+ var OPENAI_MODELS = {
3048
+ default: "gpt-4.1-nano",
3049
+ mini: "gpt-4.1-mini",
3050
+ full: "gpt-4.1"
3051
+ };
3052
+
3053
+ class OpenAIProvider extends OpenAICompatProvider {
3054
+ name = "openai";
3055
+ baseUrl = "https://api.openai.com/v1";
3056
+ authHeader = "Authorization";
3057
+ constructor(config) {
3058
+ super({
3059
+ apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
3060
+ model: config?.model ?? OPENAI_MODELS.default,
3061
+ maxTokens: config?.maxTokens ?? 1024,
3062
+ temperature: config?.temperature ?? 0,
3063
+ timeoutMs: config?.timeoutMs ?? 15000
3064
+ });
2499
3065
  }
2500
- return count;
2501
3066
  }
2502
- function archiveUnused(days, db) {
2503
- const d = db || getDatabase();
2504
- const timestamp = now();
2505
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2506
- const unusedWhere = `status = 'active' AND pinned = 0 AND access_count = 0 AND created_at < ?`;
2507
- const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${unusedWhere}`).get(cutoff).c;
2508
- if (count > 0) {
2509
- d.run(`UPDATE memories SET status = 'archived', updated_at = ? WHERE ${unusedWhere}`, [timestamp, cutoff]);
3067
+
3068
+ // src/lib/providers/cerebras.ts
3069
+ var CEREBRAS_MODELS = {
3070
+ default: "llama-3.3-70b",
3071
+ fast: "llama3.1-8b"
3072
+ };
3073
+
3074
+ class CerebrasProvider extends OpenAICompatProvider {
3075
+ name = "cerebras";
3076
+ baseUrl = "https://api.cerebras.ai/v1";
3077
+ authHeader = "Authorization";
3078
+ constructor(config) {
3079
+ super({
3080
+ apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
3081
+ model: config?.model ?? CEREBRAS_MODELS.default,
3082
+ maxTokens: config?.maxTokens ?? 1024,
3083
+ temperature: config?.temperature ?? 0,
3084
+ timeoutMs: config?.timeoutMs ?? 1e4
3085
+ });
2510
3086
  }
2511
- return count;
2512
3087
  }
2513
- function deprioritizeStale(days, db) {
2514
- const d = db || getDatabase();
2515
- const timestamp = now();
2516
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2517
- const deprioWhere = `status = 'active' AND pinned = 0 AND importance > 1 AND COALESCE(accessed_at, updated_at) < ?`;
2518
- const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${deprioWhere}`).get(cutoff).c;
2519
- if (count > 0) {
2520
- d.run(`UPDATE memories
2521
- SET importance = MAX(importance - 1, 1),
2522
- version = version + 1,
2523
- updated_at = ?
2524
- WHERE ${deprioWhere}`, [timestamp, cutoff]);
3088
+
3089
+ // src/lib/providers/grok.ts
3090
+ var GROK_MODELS = {
3091
+ default: "grok-3-mini",
3092
+ premium: "grok-3"
3093
+ };
3094
+
3095
+ class GrokProvider extends OpenAICompatProvider {
3096
+ name = "grok";
3097
+ baseUrl = "https://api.x.ai/v1";
3098
+ authHeader = "Authorization";
3099
+ constructor(config) {
3100
+ super({
3101
+ apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
3102
+ model: config?.model ?? GROK_MODELS.default,
3103
+ maxTokens: config?.maxTokens ?? 1024,
3104
+ temperature: config?.temperature ?? 0,
3105
+ timeoutMs: config?.timeoutMs ?? 15000
3106
+ });
2525
3107
  }
2526
- return count;
2527
3108
  }
2528
- function runCleanup(config, db) {
2529
- const d = db || getDatabase();
2530
- const expired = cleanExpiredMemories(d);
2531
- const evicted = enforceQuotas(config, d);
2532
- const archived = archiveStale(90, d);
2533
- const unused_archived = archiveUnused(config.auto_cleanup.unused_archive_days ?? 7, d);
2534
- const deprioritized = deprioritizeStale(config.auto_cleanup.stale_deprioritize_days ?? 14, d);
2535
- return { expired, evicted, archived, unused_archived, deprioritized };
3109
+
3110
+ // src/lib/providers/registry.ts
3111
+ class ProviderRegistry {
3112
+ config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
3113
+ _instances = new Map;
3114
+ configure(partial) {
3115
+ this.config = { ...this.config, ...partial };
3116
+ this._instances.clear();
3117
+ }
3118
+ getConfig() {
3119
+ return this.config;
3120
+ }
3121
+ getPrimary() {
3122
+ return this.getProvider(this.config.provider);
3123
+ }
3124
+ getFallbacks() {
3125
+ const fallbackNames = this.config.fallback ?? [];
3126
+ return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
3127
+ }
3128
+ getAvailable() {
3129
+ const primary = this.getPrimary();
3130
+ if (primary)
3131
+ return primary;
3132
+ const fallbacks = this.getFallbacks();
3133
+ return fallbacks[0] ?? null;
3134
+ }
3135
+ getProvider(name) {
3136
+ const cached = this._instances.get(name);
3137
+ if (cached)
3138
+ return cached;
3139
+ const provider = this.createProvider(name);
3140
+ if (!provider)
3141
+ return null;
3142
+ if (!provider.config.apiKey)
3143
+ return null;
3144
+ this._instances.set(name, provider);
3145
+ return provider;
3146
+ }
3147
+ health() {
3148
+ const providers = ["anthropic", "openai", "cerebras", "grok"];
3149
+ const result = {};
3150
+ for (const name of providers) {
3151
+ const p = this.createProvider(name);
3152
+ result[name] = {
3153
+ available: Boolean(p?.config.apiKey),
3154
+ model: p?.config.model ?? "unknown"
3155
+ };
3156
+ }
3157
+ return result;
3158
+ }
3159
+ createProvider(name) {
3160
+ const modelOverride = name === this.config.provider ? this.config.model : undefined;
3161
+ switch (name) {
3162
+ case "anthropic":
3163
+ return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
3164
+ case "openai":
3165
+ return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
3166
+ case "cerebras":
3167
+ return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
3168
+ case "grok":
3169
+ return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
3170
+ default:
3171
+ return null;
3172
+ }
3173
+ }
2536
3174
  }
2537
- // src/lib/sync.ts
2538
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2539
- import { homedir as homedir2 } from "os";
2540
- import { join as join3 } from "path";
2541
- function getAgentSyncDir(agentName) {
2542
- const dir = join3(homedir2(), ".mementos", "agents", agentName);
2543
- if (!existsSync3(dir)) {
2544
- mkdirSync3(dir, { recursive: true });
3175
+ var providerRegistry = new ProviderRegistry;
3176
+ function autoConfigureFromEnv() {
3177
+ const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
3178
+ const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
3179
+ const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
3180
+ const hasGrokKey = Boolean(process.env.XAI_API_KEY);
3181
+ if (!hasAnthropicKey) {
3182
+ if (hasCerebrasKey) {
3183
+ providerRegistry.configure({ provider: "cerebras" });
3184
+ } else if (hasOpenAIKey) {
3185
+ providerRegistry.configure({ provider: "openai" });
3186
+ } else if (hasGrokKey) {
3187
+ providerRegistry.configure({ provider: "grok" });
3188
+ }
2545
3189
  }
2546
- return dir;
3190
+ const allProviders = ["anthropic", "cerebras", "openai", "grok"];
3191
+ const available = allProviders.filter((p) => {
3192
+ switch (p) {
3193
+ case "anthropic":
3194
+ return hasAnthropicKey;
3195
+ case "cerebras":
3196
+ return hasCerebrasKey;
3197
+ case "openai":
3198
+ return hasOpenAIKey;
3199
+ case "grok":
3200
+ return hasGrokKey;
3201
+ }
3202
+ });
3203
+ const primary = providerRegistry.getConfig().provider;
3204
+ const fallback = available.filter((p) => p !== primary);
3205
+ providerRegistry.configure({ fallback });
2547
3206
  }
2548
- function setHighWaterMark(agentDir, timestamp) {
2549
- const markFile = join3(agentDir, ".highwatermark");
2550
- writeFileSync2(markFile, timestamp, "utf-8");
3207
+ autoConfigureFromEnv();
3208
+
3209
+ // src/lib/auto-memory-queue.ts
3210
+ var MAX_QUEUE_SIZE = 100;
3211
+ var CONCURRENCY = 3;
3212
+
3213
+ class AutoMemoryQueue {
3214
+ queue = [];
3215
+ handler = null;
3216
+ running = false;
3217
+ activeCount = 0;
3218
+ stats = {
3219
+ pending: 0,
3220
+ processing: 0,
3221
+ processed: 0,
3222
+ failed: 0,
3223
+ dropped: 0
3224
+ };
3225
+ setHandler(handler) {
3226
+ this.handler = handler;
3227
+ if (!this.running)
3228
+ this.startLoop();
3229
+ }
3230
+ enqueue(job) {
3231
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
3232
+ this.queue.shift();
3233
+ this.stats.dropped++;
3234
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
3235
+ }
3236
+ this.queue.push(job);
3237
+ this.stats.pending++;
3238
+ if (!this.running && this.handler)
3239
+ this.startLoop();
3240
+ }
3241
+ getStats() {
3242
+ return { ...this.stats, pending: this.queue.length };
3243
+ }
3244
+ startLoop() {
3245
+ this.running = true;
3246
+ this.loop();
3247
+ }
3248
+ async loop() {
3249
+ while (this.queue.length > 0 || this.activeCount > 0) {
3250
+ while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
3251
+ const job = this.queue.shift();
3252
+ if (!job)
3253
+ break;
3254
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
3255
+ this.activeCount++;
3256
+ this.stats.processing = this.activeCount;
3257
+ this.processJob(job);
3258
+ }
3259
+ await new Promise((r) => setImmediate(r));
3260
+ }
3261
+ this.running = false;
3262
+ }
3263
+ async processJob(job) {
3264
+ if (!this.handler) {
3265
+ this.activeCount--;
3266
+ this.stats.processing = this.activeCount;
3267
+ return;
3268
+ }
3269
+ try {
3270
+ await this.handler(job);
3271
+ this.stats.processed++;
3272
+ } catch (err) {
3273
+ this.stats.failed++;
3274
+ console.error("[auto-memory-queue] job failed:", err);
3275
+ } finally {
3276
+ this.activeCount--;
3277
+ this.stats.processing = this.activeCount;
3278
+ }
3279
+ }
2551
3280
  }
2552
- function resolveConflict(local, remote, resolution) {
2553
- switch (resolution) {
2554
- case "prefer-local":
2555
- return "local";
2556
- case "prefer-remote":
2557
- return "remote";
2558
- case "prefer-newer":
2559
- return new Date(local.updated_at).getTime() >= new Date(remote.updated_at).getTime() ? "local" : "remote";
3281
+ var autoMemoryQueue = new AutoMemoryQueue;
3282
+
3283
+ // src/lib/auto-memory.ts
3284
+ var DEDUP_SIMILARITY_THRESHOLD = 0.85;
3285
+ function isDuplicate(content, agentId, projectId) {
3286
+ try {
3287
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
3288
+ if (!query)
3289
+ return false;
3290
+ const results = searchMemories(query, {
3291
+ agent_id: agentId,
3292
+ project_id: projectId,
3293
+ limit: 3
3294
+ });
3295
+ if (results.length === 0)
3296
+ return false;
3297
+ const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
3298
+ for (const result of results) {
3299
+ const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
3300
+ if (contentWords.size === 0 || existingWords.size === 0)
3301
+ continue;
3302
+ const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
3303
+ const union = new Set([...contentWords, ...existingWords]).size;
3304
+ const similarity = intersection / union;
3305
+ if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
3306
+ return true;
3307
+ }
3308
+ return false;
3309
+ } catch {
3310
+ return false;
2560
3311
  }
2561
3312
  }
2562
- function pushMemories(agentName, agentId, projectId, db) {
2563
- const agentDir = getAgentSyncDir(agentName);
2564
- const memories = listMemories({
2565
- agent_id: agentId,
2566
- project_id: projectId,
2567
- status: "active",
2568
- limit: 1e4
2569
- }, db);
2570
- const outFile = join3(agentDir, "memories.json");
2571
- writeFileSync2(outFile, JSON.stringify(memories, null, 2), "utf-8");
2572
- if (memories.length > 0) {
2573
- const latest = memories.reduce((a, b) => new Date(a.updated_at).getTime() > new Date(b.updated_at).getTime() ? a : b);
2574
- setHighWaterMark(agentDir, latest.updated_at);
3313
+ async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
3314
+ const provider = providerRegistry.getAvailable();
3315
+ if (!provider)
3316
+ return;
3317
+ try {
3318
+ const { entities, relations } = await provider.extractEntities(content);
3319
+ const entityIdMap = new Map;
3320
+ for (const extracted of entities) {
3321
+ if (extracted.confidence < 0.6)
3322
+ continue;
3323
+ try {
3324
+ const existing = getEntityByName(extracted.name);
3325
+ const entityId = existing ? existing.id : createEntity({
3326
+ name: extracted.name,
3327
+ type: extracted.type,
3328
+ project_id: projectId
3329
+ }).id;
3330
+ entityIdMap.set(extracted.name, entityId);
3331
+ linkEntityToMemory(entityId, memoryId, "subject");
3332
+ } catch {}
3333
+ }
3334
+ for (const rel of relations) {
3335
+ const fromId = entityIdMap.get(rel.from);
3336
+ const toId = entityIdMap.get(rel.to);
3337
+ if (!fromId || !toId)
3338
+ continue;
3339
+ try {
3340
+ createRelation({
3341
+ source_entity_id: fromId,
3342
+ target_entity_id: toId,
3343
+ relation_type: rel.type
3344
+ });
3345
+ } catch {}
3346
+ }
3347
+ } catch (err) {
3348
+ console.error("[auto-memory] entity linking failed:", err);
2575
3349
  }
2576
- return memories.length;
2577
3350
  }
2578
- function pullMemories(agentName, conflictResolution = "prefer-newer", db) {
2579
- const agentDir = getAgentSyncDir(agentName);
2580
- const inFile = join3(agentDir, "memories.json");
2581
- if (!existsSync3(inFile)) {
2582
- return { pulled: 0, conflicts: 0 };
3351
+ async function saveExtractedMemory(extracted, context) {
3352
+ const minImportance = providerRegistry.getConfig().minImportance;
3353
+ if (extracted.importance < minImportance)
3354
+ return null;
3355
+ if (!extracted.content.trim())
3356
+ return null;
3357
+ if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
3358
+ return null;
2583
3359
  }
2584
- const raw = readFileSync2(inFile, "utf-8");
2585
- let remoteMemories;
2586
3360
  try {
2587
- remoteMemories = JSON.parse(raw);
2588
- } catch {
2589
- return { pulled: 0, conflicts: 0 };
2590
- }
2591
- let pulled = 0;
2592
- let conflicts = 0;
2593
- for (const remote of remoteMemories) {
2594
- const localMemories = listMemories({
2595
- search: remote.key,
2596
- scope: remote.scope,
2597
- agent_id: remote.agent_id || undefined,
2598
- project_id: remote.project_id || undefined,
2599
- limit: 1
2600
- }, db);
2601
- const local = localMemories.find((m) => m.key === remote.key);
2602
- if (local) {
2603
- const winner = resolveConflict(local, remote, conflictResolution);
2604
- if (winner === "remote") {
2605
- createMemory({
2606
- key: remote.key,
2607
- value: remote.value,
2608
- category: remote.category,
2609
- scope: remote.scope,
2610
- summary: remote.summary || undefined,
2611
- tags: remote.tags,
2612
- importance: remote.importance,
2613
- source: remote.source,
2614
- agent_id: remote.agent_id || undefined,
2615
- project_id: remote.project_id || undefined,
2616
- session_id: remote.session_id || undefined,
2617
- metadata: remote.metadata,
2618
- expires_at: remote.expires_at || undefined
2619
- }, "merge", db);
2620
- pulled++;
3361
+ const input = {
3362
+ key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
3363
+ value: extracted.content,
3364
+ category: extracted.category,
3365
+ scope: extracted.suggestedScope,
3366
+ importance: extracted.importance,
3367
+ tags: [
3368
+ ...extracted.tags,
3369
+ "auto-extracted",
3370
+ ...context.sessionId ? [`session:${context.sessionId}`] : []
3371
+ ],
3372
+ agent_id: context.agentId,
3373
+ project_id: context.projectId,
3374
+ session_id: context.sessionId,
3375
+ metadata: {
3376
+ reasoning: extracted.reasoning,
3377
+ auto_extracted: true,
3378
+ extracted_at: new Date().toISOString()
2621
3379
  }
2622
- conflicts++;
2623
- } else {
2624
- createMemory({
2625
- key: remote.key,
2626
- value: remote.value,
2627
- category: remote.category,
2628
- scope: remote.scope,
2629
- summary: remote.summary || undefined,
2630
- tags: remote.tags,
2631
- importance: remote.importance,
2632
- source: remote.source,
2633
- agent_id: remote.agent_id || undefined,
2634
- project_id: remote.project_id || undefined,
2635
- session_id: remote.session_id || undefined,
2636
- metadata: remote.metadata,
2637
- expires_at: remote.expires_at || undefined
2638
- }, "create", db);
2639
- pulled++;
2640
- }
3380
+ };
3381
+ const memory = createMemory(input, "merge");
3382
+ return memory.id;
3383
+ } catch (err) {
3384
+ console.error("[auto-memory] saveExtractedMemory failed:", err);
3385
+ return null;
2641
3386
  }
2642
- return { pulled, conflicts };
2643
3387
  }
2644
- function syncMemories(agentName, direction = "both", options = {}) {
2645
- const result = {
2646
- pushed: 0,
2647
- pulled: 0,
2648
- conflicts: 0,
2649
- errors: []
3388
+ async function processJob(job) {
3389
+ if (!providerRegistry.getConfig().enabled)
3390
+ return;
3391
+ const provider = providerRegistry.getAvailable();
3392
+ if (!provider)
3393
+ return;
3394
+ const context = {
3395
+ agentId: job.agentId,
3396
+ projectId: job.projectId,
3397
+ sessionId: job.sessionId
2650
3398
  };
3399
+ let extracted = [];
2651
3400
  try {
2652
- if (direction === "push" || direction === "both") {
2653
- result.pushed = pushMemories(agentName, options.agent_id, options.project_id, options.db);
3401
+ extracted = await provider.extractMemories(job.turn, context);
3402
+ } catch {
3403
+ const fallbacks = providerRegistry.getFallbacks();
3404
+ for (const fallback of fallbacks) {
3405
+ try {
3406
+ extracted = await fallback.extractMemories(job.turn, context);
3407
+ if (extracted.length > 0)
3408
+ break;
3409
+ } catch {
3410
+ continue;
3411
+ }
2654
3412
  }
2655
- if (direction === "pull" || direction === "both") {
2656
- const pullResult = pullMemories(agentName, options.conflict_resolution || "prefer-newer", options.db);
2657
- result.pulled = pullResult.pulled;
2658
- result.conflicts = pullResult.conflicts;
3413
+ }
3414
+ if (extracted.length === 0)
3415
+ return;
3416
+ for (const memory of extracted) {
3417
+ const memoryId = await saveExtractedMemory(memory, context);
3418
+ if (!memoryId)
3419
+ continue;
3420
+ if (providerRegistry.getConfig().autoEntityLink) {
3421
+ linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
2659
3422
  }
2660
- } catch (e) {
2661
- result.errors.push(e instanceof Error ? e.message : String(e));
2662
3423
  }
2663
- return result;
2664
3424
  }
2665
- var defaultSyncAgents = ["claude", "codex", "gemini"];
3425
+ autoMemoryQueue.setHandler(processJob);
3426
+ function processConversationTurn(turn, context, source = "turn") {
3427
+ if (!turn?.trim())
3428
+ return;
3429
+ autoMemoryQueue.enqueue({
3430
+ ...context,
3431
+ turn,
3432
+ timestamp: Date.now(),
3433
+ source
3434
+ });
3435
+ }
3436
+ function getAutoMemoryStats() {
3437
+ return autoMemoryQueue.getStats();
3438
+ }
3439
+ function configureAutoMemory(config) {
3440
+ providerRegistry.configure(config);
3441
+ }
3442
+ // src/lib/dedup.ts
3443
+ var DEFAULT_CONFIG2 = {
3444
+ threshold: 0.8,
3445
+ keepLonger: true
3446
+ };
3447
+ var _stats = { checked: 0, skipped: 0, updated: 0 };
3448
+ function getDedupStats() {
3449
+ return { ..._stats };
3450
+ }
3451
+ function checkDuplicate(content, filter, config = DEFAULT_CONFIG2) {
3452
+ _stats.checked++;
3453
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
3454
+ if (!query)
3455
+ return "unique";
3456
+ let results;
3457
+ try {
3458
+ results = searchMemories(query, { ...filter, limit: 5 });
3459
+ } catch {
3460
+ return "unique";
3461
+ }
3462
+ if (results.length === 0)
3463
+ return "unique";
3464
+ const contentWords = tokenize(content);
3465
+ if (contentWords.size === 0)
3466
+ return "unique";
3467
+ for (const result of results) {
3468
+ const existingWords = tokenize(result.memory.value);
3469
+ if (existingWords.size === 0)
3470
+ continue;
3471
+ const similarity = jaccardSimilarity(contentWords, existingWords);
3472
+ if (similarity >= config.threshold) {
3473
+ if (config.keepLonger && content.length > result.memory.value.length) {
3474
+ return { updateId: result.memory.id, existingContent: result.memory.value };
3475
+ }
3476
+ return "duplicate";
3477
+ }
3478
+ }
3479
+ return "unique";
3480
+ }
3481
+ function dedup(content, filter, config = DEFAULT_CONFIG2) {
3482
+ const result = checkDuplicate(content, filter, config);
3483
+ if (result === "unique")
3484
+ return "save";
3485
+ if (result === "duplicate") {
3486
+ _stats.skipped++;
3487
+ return "skip";
3488
+ }
3489
+ try {
3490
+ const existing = getMemory(result.updateId);
3491
+ if (!existing)
3492
+ return "save";
3493
+ updateMemory(result.updateId, { value: content, version: existing.version });
3494
+ _stats.updated++;
3495
+ } catch {
3496
+ return "save";
3497
+ }
3498
+ return "skip";
3499
+ }
3500
+ function tokenize(text) {
3501
+ return new Set(text.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
3502
+ }
3503
+ function jaccardSimilarity(a, b) {
3504
+ if (a.size === 0 || b.size === 0)
3505
+ return 0;
3506
+ const intersection = [...a].filter((w) => b.has(w)).length;
3507
+ const union = new Set([...a, ...b]).size;
3508
+ return intersection / union;
3509
+ }
2666
3510
  export {
2667
3511
  withMemoryLock,
2668
3512
  uuid,
@@ -2670,13 +3514,16 @@ export {
2670
3514
  updateEntity,
2671
3515
  updateAgent,
2672
3516
  unlinkEntityFromMemory,
3517
+ unfocus,
2673
3518
  touchMemory,
2674
3519
  touchAgent,
2675
3520
  syncMemories,
2676
3521
  shortUuid,
3522
+ setFocus,
2677
3523
  setActiveProfile,
2678
3524
  searchMemories,
2679
3525
  runCleanup,
3526
+ resolveProjectId,
2680
3527
  resolvePartialId,
2681
3528
  resetDatabase,
2682
3529
  releaseResourceLocks,
@@ -2686,8 +3533,10 @@ export {
2686
3533
  registerProject,
2687
3534
  registerAgent,
2688
3535
  redactSecrets,
3536
+ providerRegistry,
3537
+ processConversationTurn,
2689
3538
  parseRelationRow,
2690
- parseEntityRow,
3539
+ parseEntityRow2 as parseEntityRow,
2691
3540
  now,
2692
3541
  mergeEntities,
2693
3542
  memoryLockId,
@@ -2701,6 +3550,7 @@ export {
2701
3550
  listAgents,
2702
3551
  listAgentLocks,
2703
3552
  linkEntityToMemory,
3553
+ incrementRecallCount,
2704
3554
  getRelation,
2705
3555
  getRelatedEntities,
2706
3556
  getProject,
@@ -2709,17 +3559,20 @@ export {
2709
3559
  getMemory,
2710
3560
  getMemoriesForEntity,
2711
3561
  getMemoriesByKey,
3562
+ getFocus,
2712
3563
  getEntityMemoryLinks,
2713
3564
  getEntityGraph,
2714
3565
  getEntityByName,
2715
3566
  getEntity,
2716
3567
  getEntitiesForMemory,
3568
+ getDedupStats,
2717
3569
  getDbPath,
2718
3570
  getDatabase,
3571
+ getAutoMemoryStats,
2719
3572
  getAgent,
2720
3573
  getActiveProfile,
3574
+ focusFilterSQL,
2721
3575
  findPath,
2722
- extractEntities,
2723
3576
  enforceQuotas,
2724
3577
  deprioritizeStale,
2725
3578
  deleteRelation,
@@ -2727,10 +3580,12 @@ export {
2727
3580
  deleteMemory,
2728
3581
  deleteEntity,
2729
3582
  defaultSyncAgents,
3583
+ dedup,
2730
3584
  createRelation,
2731
3585
  createMemory,
2732
3586
  createEntity,
2733
3587
  containsSecrets,
3588
+ configureAutoMemory,
2734
3589
  closeDatabase,
2735
3590
  cleanExpiredMemories,
2736
3591
  cleanExpiredLocks,
@@ -2738,6 +3593,7 @@ export {
2738
3593
  checkLock,
2739
3594
  bulkLinkEntities,
2740
3595
  bulkDeleteMemories,
3596
+ buildFocusFilter,
2741
3597
  archiveUnused,
2742
3598
  archiveStale,
2743
3599
  agentHoldsLock,