@hasna/mementos 0.4.41 → 0.6.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 (38) hide show
  1. package/dist/cli/index.js +2247 -1548
  2. package/dist/db/database.d.ts.map +1 -1
  3. package/dist/db/memories.d.ts +1 -0
  4. package/dist/db/memories.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2041 -1336
  8. package/dist/lib/auto-memory-queue.d.ts +46 -0
  9. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  10. package/dist/lib/auto-memory.d.ts +18 -0
  11. package/dist/lib/auto-memory.d.ts.map +1 -0
  12. package/dist/lib/dedup.d.ts +33 -0
  13. package/dist/lib/dedup.d.ts.map +1 -0
  14. package/dist/lib/focus.d.ts +58 -0
  15. package/dist/lib/focus.d.ts.map +1 -0
  16. package/dist/lib/providers/anthropic.d.ts +21 -0
  17. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  18. package/dist/lib/providers/base.d.ts +96 -0
  19. package/dist/lib/providers/base.d.ts.map +1 -0
  20. package/dist/lib/providers/cerebras.d.ts +20 -0
  21. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  22. package/dist/lib/providers/grok.d.ts +19 -0
  23. package/dist/lib/providers/grok.d.ts.map +1 -0
  24. package/dist/lib/providers/index.d.ts +7 -0
  25. package/dist/lib/providers/index.d.ts.map +1 -0
  26. package/dist/lib/providers/openai-compat.d.ts +18 -0
  27. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  28. package/dist/lib/providers/openai.d.ts +20 -0
  29. package/dist/lib/providers/openai.d.ts.map +1 -0
  30. package/dist/lib/providers/registry.d.ts +38 -0
  31. package/dist/lib/providers/registry.d.ts.map +1 -0
  32. package/dist/lib/search.d.ts.map +1 -1
  33. package/dist/mcp/index.js +1781 -1101
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/index.js +1480 -941
  36. package/dist/types/index.d.ts +7 -0
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -341,6 +341,11 @@ 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);
344
349
  `
345
350
  ];
346
351
  var _db = null;
@@ -438,1387 +443,1026 @@ function containsSecrets(text) {
438
443
  return false;
439
444
  }
440
445
 
441
- // src/db/agents.ts
442
- var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
443
- function parseAgentRow(row) {
446
+ // src/db/entity-memories.ts
447
+ function parseEntityRow(row) {
444
448
  return {
445
449
  id: row["id"],
446
450
  name: row["name"],
447
- session_id: row["session_id"] || null,
451
+ type: row["type"],
448
452
  description: row["description"] || null,
449
- role: row["role"] || null,
450
453
  metadata: JSON.parse(row["metadata"] || "{}"),
451
- active_project_id: row["active_project_id"] || null,
454
+ project_id: row["project_id"] || null,
452
455
  created_at: row["created_at"],
453
- last_seen_at: row["last_seen_at"]
456
+ updated_at: row["updated_at"]
454
457
  };
455
458
  }
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);
459
+ function parseEntityMemoryRow(row) {
460
+ return {
461
+ entity_id: row["entity_id"],
462
+ memory_id: row["memory_id"],
463
+ role: row["role"],
464
+ created_at: row["created_at"]
465
+ };
497
466
  }
498
- function getAgent(idOrName, db) {
467
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
499
468
  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;
469
+ const timestamp = now();
470
+ d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
471
+ VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
472
+ const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
473
+ return parseEntityMemoryRow(row);
510
474
  }
511
- function listAgents(db) {
475
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
512
476
  const d = db || getDatabase();
513
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
514
- return rows.map(parseAgentRow);
477
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
515
478
  }
516
- function touchAgent(idOrName, db) {
479
+ function getMemoriesForEntity(entityId, db) {
517
480
  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]);
481
+ const rows = d.query(`SELECT m.* FROM memories m
482
+ INNER JOIN entity_memories em ON em.memory_id = m.id
483
+ WHERE em.entity_id = ?
484
+ ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
485
+ return rows.map(parseMemoryRow);
522
486
  }
523
- function listAgentsByProject(projectId, db) {
487
+ function getEntitiesForMemory(memoryId, db) {
524
488
  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);
489
+ const rows = d.query(`SELECT e.* FROM entities e
490
+ INNER JOIN entity_memories em ON em.entity_id = e.id
491
+ WHERE em.memory_id = ?
492
+ ORDER BY e.name ASC`).all(memoryId);
493
+ return rows.map(parseEntityRow);
527
494
  }
528
- function updateAgent(id, updates, db) {
495
+ function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
529
496
  const d = db || getDatabase();
530
- const agent = getAgent(id, d);
531
- if (!agent)
532
- return null;
533
497
  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]);
498
+ const tx = d.transaction(() => {
499
+ const stmt = d.prepare(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
500
+ VALUES (?, ?, ?, ?)`);
501
+ for (const entityId of entityIds) {
502
+ stmt.run(entityId, memoryId, role, timestamp);
542
503
  }
543
- }
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]);
552
- }
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]);
555
- }
556
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
557
- return getAgent(agent.id, d);
558
- }
559
-
560
- // src/db/projects.ts
561
- function parseProjectRow(row) {
562
- return {
563
- id: row["id"],
564
- name: row["name"],
565
- path: row["path"],
566
- description: row["description"] || null,
567
- memory_prefix: row["memory_prefix"] || null,
568
- created_at: row["created_at"],
569
- updated_at: row["updated_at"]
570
- };
571
- }
572
- function registerProject(name, path, description, memoryPrefix, db) {
573
- const d = db || getDatabase();
574
- 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);
583
- }
584
- 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;
504
+ });
505
+ tx();
600
506
  }
601
- function listProjects(db) {
507
+ function getEntityMemoryLinks(entityId, memoryId, db) {
602
508
  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);
509
+ const conditions = [];
510
+ const params = [];
511
+ if (entityId) {
512
+ conditions.push("entity_id = ?");
513
+ params.push(entityId);
677
514
  }
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
- }
515
+ if (memoryId) {
516
+ conditions.push("memory_id = ?");
517
+ params.push(memoryId);
706
518
  }
707
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
708
- add(match[1], "concept", 0.5);
519
+ let sql = "SELECT * FROM entity_memories";
520
+ if (conditions.length > 0) {
521
+ sql += ` WHERE ${conditions.join(" AND ")}`;
709
522
  }
710
- return Array.from(entityMap.values()).sort((a, b) => b.confidence - a.confidence);
523
+ sql += " ORDER BY created_at DESC";
524
+ const rows = d.query(sql).all(...params);
525
+ return rows.map(parseEntityMemoryRow);
711
526
  }
712
527
 
713
- // src/db/entities.ts
714
- function parseEntityRow(row) {
528
+ // src/db/memories.ts
529
+ function runEntityExtraction(_memory, _projectId, _d) {}
530
+ function parseMemoryRow(row) {
715
531
  return {
716
532
  id: row["id"],
717
- name: row["name"],
718
- type: row["type"],
719
- description: row["description"] || null,
720
- metadata: JSON.parse(row["metadata"] || "{}"),
533
+ key: row["key"],
534
+ value: row["value"],
535
+ category: row["category"],
536
+ scope: row["scope"],
537
+ summary: row["summary"] || null,
538
+ tags: JSON.parse(row["tags"] || "[]"),
539
+ importance: row["importance"],
540
+ source: row["source"],
541
+ status: row["status"],
542
+ pinned: !!row["pinned"],
543
+ agent_id: row["agent_id"] || null,
721
544
  project_id: row["project_id"] || null,
545
+ session_id: row["session_id"] || null,
546
+ metadata: JSON.parse(row["metadata"] || "{}"),
547
+ access_count: row["access_count"],
548
+ version: row["version"],
549
+ expires_at: row["expires_at"] || null,
722
550
  created_at: row["created_at"],
723
- updated_at: row["updated_at"]
551
+ updated_at: row["updated_at"],
552
+ accessed_at: row["accessed_at"] || null
724
553
  };
725
554
  }
726
- function createEntity(input, db) {
555
+ function createMemory(input, dedupeMode = "merge", db) {
727
556
  const d = db || getDatabase();
728
557
  const timestamp = now();
558
+ let expiresAt = input.expires_at || null;
559
+ if (input.ttl_ms && !expiresAt) {
560
+ expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
561
+ }
562
+ const id = uuid();
563
+ const tags = input.tags || [];
564
+ const tagsJson = JSON.stringify(tags);
729
565
  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);
566
+ const safeValue = redactSecrets(input.value);
567
+ const safeSummary = input.summary ? redactSecrets(input.summary) : null;
568
+ if (dedupeMode === "merge") {
569
+ const existing = d.query(`SELECT id, version FROM memories
570
+ WHERE key = ? AND scope = ?
571
+ AND COALESCE(agent_id, '') = ?
572
+ AND COALESCE(project_id, '') = ?
573
+ AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
574
+ if (existing) {
575
+ d.run(`UPDATE memories SET
576
+ value = ?, category = ?, summary = ?, tags = ?,
577
+ importance = ?, metadata = ?, expires_at = ?,
578
+ pinned = COALESCE(pinned, 0),
579
+ version = version + 1, updated_at = ?
580
+ WHERE id = ?`, [
581
+ safeValue,
582
+ input.category || "knowledge",
583
+ safeSummary,
584
+ tagsJson,
585
+ input.importance ?? 5,
586
+ metadataJson,
587
+ expiresAt,
588
+ timestamp,
589
+ existing.id
590
+ ]);
591
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
592
+ const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
593
+ for (const tag of tags) {
594
+ insertTag2.run(existing.id, tag);
595
+ }
596
+ const merged = getMemory(existing.id, d);
597
+ try {
598
+ const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
599
+ for (const link of oldLinks) {
600
+ unlinkEntityFromMemory(link.entity_id, merged.id, d);
601
+ }
602
+ runEntityExtraction(merged, input.project_id, d);
603
+ } catch {}
604
+ return merged;
742
605
  }
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
606
  }
748
- const id = shortUuid();
749
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
750
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
607
+ 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)
608
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, 0, 1, ?, ?, ?)`, [
751
609
  id,
752
- input.name,
753
- input.type,
754
- input.description || null,
755
- metadataJson,
610
+ input.key,
611
+ input.value,
612
+ input.category || "knowledge",
613
+ input.scope || "private",
614
+ input.summary || null,
615
+ tagsJson,
616
+ input.importance ?? 5,
617
+ input.source || "agent",
618
+ input.agent_id || null,
756
619
  input.project_id || null,
620
+ input.session_id || null,
621
+ metadataJson,
622
+ expiresAt,
757
623
  timestamp,
758
624
  timestamp
759
625
  ]);
760
- return getEntity(id, d);
626
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
627
+ for (const tag of tags) {
628
+ insertTag.run(id, tag);
629
+ }
630
+ const memory = getMemory(id, d);
631
+ try {
632
+ runEntityExtraction(memory, input.project_id, d);
633
+ } catch {}
634
+ return memory;
761
635
  }
762
- function getEntity(id, db) {
636
+ function getMemory(id, db) {
763
637
  const d = db || getDatabase();
764
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
638
+ const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
765
639
  if (!row)
766
- throw new EntityNotFoundError(id);
767
- return parseEntityRow(row);
640
+ return null;
641
+ return parseMemoryRow(row);
768
642
  }
769
- function getEntityByName(name, type, projectId, db) {
643
+ function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
770
644
  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);
645
+ let sql = "SELECT * FROM memories WHERE key = ?";
646
+ const params = [key];
647
+ if (scope) {
648
+ sql += " AND scope = ?";
649
+ params.push(scope);
776
650
  }
777
- if (projectId !== undefined) {
651
+ if (agentId) {
652
+ sql += " AND agent_id = ?";
653
+ params.push(agentId);
654
+ }
655
+ if (projectId) {
778
656
  sql += " AND project_id = ?";
779
657
  params.push(projectId);
780
658
  }
781
- sql += " LIMIT 1";
659
+ if (sessionId) {
660
+ sql += " AND session_id = ?";
661
+ params.push(sessionId);
662
+ }
663
+ sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
782
664
  const row = d.query(sql).get(...params);
783
665
  if (!row)
784
666
  return null;
785
- return parseEntityRow(row);
667
+ return parseMemoryRow(row);
786
668
  }
787
- function listEntities(filter = {}, db) {
669
+ function getMemoriesByKey(key, scope, agentId, projectId, db) {
788
670
  const d = db || getDatabase();
789
- const conditions = [];
790
- const params = [];
791
- if (filter.type) {
792
- conditions.push("type = ?");
793
- params.push(filter.type);
671
+ let sql = "SELECT * FROM memories WHERE key = ?";
672
+ const params = [key];
673
+ if (scope) {
674
+ sql += " AND scope = ?";
675
+ params.push(scope);
794
676
  }
795
- if (filter.project_id) {
796
- conditions.push("project_id = ?");
797
- params.push(filter.project_id);
677
+ if (agentId) {
678
+ sql += " AND agent_id = ?";
679
+ params.push(agentId);
798
680
  }
799
- if (filter.search) {
800
- conditions.push("(name LIKE ? OR description LIKE ?)");
801
- const term = `%${filter.search}%`;
802
- params.push(term, term);
681
+ if (projectId) {
682
+ sql += " AND project_id = ?";
683
+ params.push(projectId);
803
684
  }
804
- let sql = "SELECT * FROM entities";
685
+ sql += " AND status = 'active' ORDER BY importance DESC";
686
+ const rows = d.query(sql).all(...params);
687
+ return rows.map(parseMemoryRow);
688
+ }
689
+ function listMemories(filter, db) {
690
+ const d = db || getDatabase();
691
+ const conditions = [];
692
+ const params = [];
693
+ if (filter) {
694
+ if (filter.scope) {
695
+ if (Array.isArray(filter.scope)) {
696
+ conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
697
+ params.push(...filter.scope);
698
+ } else {
699
+ conditions.push("scope = ?");
700
+ params.push(filter.scope);
701
+ }
702
+ }
703
+ if (filter.category) {
704
+ if (Array.isArray(filter.category)) {
705
+ conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
706
+ params.push(...filter.category);
707
+ } else {
708
+ conditions.push("category = ?");
709
+ params.push(filter.category);
710
+ }
711
+ }
712
+ if (filter.source) {
713
+ if (Array.isArray(filter.source)) {
714
+ conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
715
+ params.push(...filter.source);
716
+ } else {
717
+ conditions.push("source = ?");
718
+ params.push(filter.source);
719
+ }
720
+ }
721
+ if (filter.status) {
722
+ if (Array.isArray(filter.status)) {
723
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
724
+ params.push(...filter.status);
725
+ } else {
726
+ conditions.push("status = ?");
727
+ params.push(filter.status);
728
+ }
729
+ } else {
730
+ conditions.push("status = 'active'");
731
+ }
732
+ if (filter.project_id) {
733
+ conditions.push("project_id = ?");
734
+ params.push(filter.project_id);
735
+ }
736
+ if (filter.agent_id) {
737
+ conditions.push("agent_id = ?");
738
+ params.push(filter.agent_id);
739
+ }
740
+ if (filter.session_id) {
741
+ conditions.push("session_id = ?");
742
+ params.push(filter.session_id);
743
+ }
744
+ if (filter.min_importance) {
745
+ conditions.push("importance >= ?");
746
+ params.push(filter.min_importance);
747
+ }
748
+ if (filter.pinned !== undefined) {
749
+ conditions.push("pinned = ?");
750
+ params.push(filter.pinned ? 1 : 0);
751
+ }
752
+ if (filter.tags && filter.tags.length > 0) {
753
+ for (const tag of filter.tags) {
754
+ conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
755
+ params.push(tag);
756
+ }
757
+ }
758
+ if (filter.search) {
759
+ conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
760
+ const term = `%${filter.search}%`;
761
+ params.push(term, term, term);
762
+ }
763
+ } else {
764
+ conditions.push("status = 'active'");
765
+ }
766
+ let sql = "SELECT * FROM memories";
805
767
  if (conditions.length > 0) {
806
768
  sql += ` WHERE ${conditions.join(" AND ")}`;
807
769
  }
808
- sql += " ORDER BY updated_at DESC";
809
- if (filter.limit) {
770
+ sql += " ORDER BY importance DESC, created_at DESC";
771
+ if (filter?.limit) {
810
772
  sql += " LIMIT ?";
811
773
  params.push(filter.limit);
812
774
  }
813
- if (filter.offset) {
775
+ if (filter?.offset) {
814
776
  sql += " OFFSET ?";
815
777
  params.push(filter.offset);
816
778
  }
817
779
  const rows = d.query(sql).all(...params);
818
- return rows.map(parseEntityRow);
780
+ return rows.map(parseMemoryRow);
819
781
  }
820
- function updateEntity(id, input, db) {
782
+ function updateMemory(id, input, db) {
821
783
  const d = db || getDatabase();
822
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
784
+ const existing = getMemory(id, d);
823
785
  if (!existing)
824
- throw new EntityNotFoundError(id);
825
- const sets = ["updated_at = ?"];
826
- const params = [now()];
827
- if (input.name !== undefined) {
828
- sets.push("name = ?");
829
- params.push(input.name);
830
- }
831
- if (input.type !== undefined) {
832
- sets.push("type = ?");
833
- params.push(input.type);
834
- }
835
- if (input.description !== undefined) {
836
- sets.push("description = ?");
837
- params.push(input.description);
786
+ throw new MemoryNotFoundError(id);
787
+ if (existing.version !== input.version) {
788
+ throw new VersionConflictError(id, input.version, existing.version);
838
789
  }
839
- if (input.metadata !== undefined) {
790
+ try {
791
+ d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
792
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
793
+ uuid(),
794
+ existing.id,
795
+ existing.version,
796
+ existing.value,
797
+ existing.importance,
798
+ existing.scope,
799
+ existing.category,
800
+ JSON.stringify(existing.tags),
801
+ existing.summary,
802
+ existing.pinned ? 1 : 0,
803
+ existing.status,
804
+ existing.updated_at
805
+ ]);
806
+ } catch {}
807
+ const sets = ["version = version + 1", "updated_at = ?"];
808
+ const params = [now()];
809
+ if (input.value !== undefined) {
810
+ sets.push("value = ?");
811
+ params.push(redactSecrets(input.value));
812
+ }
813
+ if (input.category !== undefined) {
814
+ sets.push("category = ?");
815
+ params.push(input.category);
816
+ }
817
+ if (input.scope !== undefined) {
818
+ sets.push("scope = ?");
819
+ params.push(input.scope);
820
+ }
821
+ if (input.summary !== undefined) {
822
+ sets.push("summary = ?");
823
+ params.push(input.summary);
824
+ }
825
+ if (input.importance !== undefined) {
826
+ sets.push("importance = ?");
827
+ params.push(input.importance);
828
+ }
829
+ if (input.pinned !== undefined) {
830
+ sets.push("pinned = ?");
831
+ params.push(input.pinned ? 1 : 0);
832
+ }
833
+ if (input.status !== undefined) {
834
+ sets.push("status = ?");
835
+ params.push(input.status);
836
+ }
837
+ if (input.metadata !== undefined) {
840
838
  sets.push("metadata = ?");
841
839
  params.push(JSON.stringify(input.metadata));
842
840
  }
841
+ if (input.expires_at !== undefined) {
842
+ sets.push("expires_at = ?");
843
+ params.push(input.expires_at);
844
+ }
845
+ if (input.tags !== undefined) {
846
+ sets.push("tags = ?");
847
+ params.push(JSON.stringify(input.tags));
848
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
849
+ const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
850
+ for (const tag of input.tags) {
851
+ insertTag.run(id, tag);
852
+ }
853
+ }
843
854
  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
- };
855
+ d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
856
+ const updated = getMemory(id, d);
857
+ try {
858
+ if (input.value !== undefined) {
859
+ const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
860
+ for (const link of oldLinks) {
861
+ unlinkEntityFromMemory(link.entity_id, updated.id, d);
862
+ }
863
+ runEntityExtraction(updated, existing.project_id || undefined, d);
864
+ }
865
+ } catch {}
866
+ return updated;
890
867
  }
891
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
868
+ function deleteMemory(id, db) {
892
869
  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);
870
+ const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
871
+ return result.changes > 0;
898
872
  }
899
- function unlinkEntityFromMemory(entityId, memoryId, db) {
873
+ function bulkDeleteMemories(ids, db) {
900
874
  const d = db || getDatabase();
901
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
875
+ if (ids.length === 0)
876
+ return 0;
877
+ const placeholders = ids.map(() => "?").join(",");
878
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
879
+ const count = countRow.c;
880
+ if (count > 0) {
881
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
882
+ }
883
+ return count;
902
884
  }
903
- function getMemoriesForEntity(entityId, db) {
885
+ function touchMemory(id, db) {
904
886
  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);
887
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
910
888
  }
911
- function getEntitiesForMemory(memoryId, db) {
889
+ var RECALL_PROMOTE_THRESHOLD = 3;
890
+ function incrementRecallCount(id, db) {
912
891
  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);
892
+ try {
893
+ d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
894
+ const row = d.query("SELECT recall_count, importance FROM memories WHERE id = ?").get(id);
895
+ if (!row)
896
+ return;
897
+ const promotions = Math.floor(row.recall_count / RECALL_PROMOTE_THRESHOLD);
898
+ if (promotions > 0 && row.importance < 10) {
899
+ const newImportance = Math.min(10, row.importance + 1);
900
+ d.run("UPDATE memories SET importance = ? WHERE id = ? AND importance < 10", [newImportance, id]);
901
+ }
902
+ } catch {}
918
903
  }
919
- function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
904
+ function cleanExpiredMemories(db) {
920
905
  const d = db || getDatabase();
921
906
  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();
907
+ const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
908
+ const count = countRow.c;
909
+ if (count > 0) {
910
+ d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
911
+ }
912
+ return count;
930
913
  }
931
- function getEntityMemoryLinks(entityId, memoryId, db) {
914
+ function getMemoryVersions(memoryId, db) {
932
915
  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 ")}`;
916
+ try {
917
+ const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
918
+ return rows.map((row) => ({
919
+ id: row["id"],
920
+ memory_id: row["memory_id"],
921
+ version: row["version"],
922
+ value: row["value"],
923
+ importance: row["importance"],
924
+ scope: row["scope"],
925
+ category: row["category"],
926
+ tags: JSON.parse(row["tags"] || "[]"),
927
+ summary: row["summary"] || null,
928
+ pinned: !!row["pinned"],
929
+ status: row["status"],
930
+ created_at: row["created_at"]
931
+ }));
932
+ } catch {
933
+ return [];
946
934
  }
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
935
  }
964
- function parseEntityRow3(row) {
936
+ // src/db/agents.ts
937
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
938
+ function parseAgentRow(row) {
965
939
  return {
966
940
  id: row["id"],
967
941
  name: row["name"],
968
- type: row["type"],
942
+ session_id: row["session_id"] || null,
969
943
  description: row["description"] || null,
944
+ role: row["role"] || null,
970
945
  metadata: JSON.parse(row["metadata"] || "{}"),
971
- project_id: row["project_id"] || null,
946
+ active_project_id: row["active_project_id"] || null,
972
947
  created_at: row["created_at"],
973
- updated_at: row["updated_at"]
948
+ last_seen_at: row["last_seen_at"]
974
949
  };
975
950
  }
976
- function createRelation(input, db) {
951
+ function registerAgent(name, sessionId, description, role, projectId, db) {
977
952
  const d = db || getDatabase();
978
- const id = shortUuid();
979
953
  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);
989
- }
990
- function getRelation(id, db) {
991
- 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);
996
- }
997
- function listRelations(filter, db) {
998
- 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);
954
+ const normalizedName = name.trim().toLowerCase();
955
+ const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
956
+ if (existing) {
957
+ const existingId = existing["id"];
958
+ const existingSessionId = existing["session_id"] || null;
959
+ const existingLastSeen = existing["last_seen_at"];
960
+ if (sessionId && existingSessionId && existingSessionId !== sessionId) {
961
+ const lastSeenMs = new Date(existingLastSeen).getTime();
962
+ const nowMs = Date.now();
963
+ if (nowMs - lastSeenMs < CONFLICT_WINDOW_MS) {
964
+ throw new AgentConflictError({
965
+ existing_id: existingId,
966
+ existing_name: normalizedName,
967
+ last_seen_at: existingLastSeen,
968
+ session_hint: existingSessionId.slice(0, 8),
969
+ working_dir: null
970
+ });
971
+ }
1012
972
  }
973
+ d.run("UPDATE agents SET last_seen_at = ?, session_id = ? WHERE id = ?", [
974
+ timestamp,
975
+ sessionId ?? existingSessionId,
976
+ existingId
977
+ ]);
978
+ if (description) {
979
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [description, existingId]);
980
+ }
981
+ if (role) {
982
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [role, existingId]);
983
+ }
984
+ if (projectId !== undefined) {
985
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [projectId, existingId]);
986
+ }
987
+ return getAgent(existingId, d);
1013
988
  }
1014
- if (filter.relation_type) {
1015
- conditions.push("relation_type = ?");
1016
- params.push(filter.relation_type);
1017
- }
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);
989
+ const id = shortUuid();
990
+ 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]);
991
+ return getAgent(id, d);
1021
992
  }
1022
- function deleteRelation(id, db) {
993
+ function getAgent(idOrName, db) {
1023
994
  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}`);
995
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
996
+ if (row)
997
+ return parseAgentRow(row);
998
+ row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
999
+ if (row)
1000
+ return parseAgentRow(row);
1001
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
1002
+ if (rows.length === 1)
1003
+ return parseAgentRow(rows[0]);
1004
+ return null;
1027
1005
  }
1028
- function getRelatedEntities(entityId, relationType, db) {
1006
+ function listAgents(db) {
1029
1007
  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);
1051
- }
1052
- const rows = d.query(sql).all(...params);
1053
- return rows.map(parseEntityRow3);
1008
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
1009
+ return rows.map(parseAgentRow);
1054
1010
  }
1055
- function getEntityGraph(entityId, depth = 2, db) {
1011
+ function touchAgent(idOrName, db) {
1056
1012
  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 };
1013
+ const agent = getAgent(idOrName, d);
1014
+ if (!agent)
1015
+ return;
1016
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), agent.id]);
1076
1017
  }
1077
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
1018
+ function listAgentsByProject(projectId, db) {
1078
1019
  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;
1020
+ const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
1021
+ return rows.map(parseAgentRow);
1101
1022
  }
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;
1023
+ function updateAgent(id, updates, db) {
1024
+ const d = db || getDatabase();
1025
+ const agent = getAgent(id, d);
1026
+ if (!agent)
1027
+ return null;
1028
+ const timestamp = now();
1029
+ if (updates.name) {
1030
+ const normalizedNewName = updates.name.trim().toLowerCase();
1031
+ if (normalizedNewName !== agent.name) {
1032
+ const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
1033
+ if (existing) {
1034
+ throw new Error(`Agent name already taken: ${normalizedNewName}`);
1035
+ }
1036
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [normalizedNewName, agent.id]);
1144
1037
  }
1145
1038
  }
1146
- return result;
1147
- }
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);
1157
- }
1158
- function isValidCategory(value) {
1159
- return VALID_CATEGORIES.includes(value);
1160
- }
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 {}
1039
+ if (updates.description !== undefined) {
1040
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
1169
1041
  }
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;
1042
+ if (updates.role !== undefined) {
1043
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
1174
1044
  }
1175
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
1176
- if (envCategory && isValidCategory(envCategory)) {
1177
- merged.default_category = envCategory;
1045
+ if (updates.metadata !== undefined) {
1046
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
1178
1047
  }
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
- }
1048
+ if ("active_project_id" in updates) {
1049
+ d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
1185
1050
  }
1186
- return merged;
1187
- }
1188
- function profilesDir() {
1189
- return join2(homedir(), ".mementos", "profiles");
1051
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
1052
+ return getAgent(agent.id, d);
1190
1053
  }
1191
- function globalConfigPath() {
1192
- return join2(homedir(), ".mementos", "config.json");
1054
+ // src/db/locks.ts
1055
+ function parseLockRow(row) {
1056
+ return {
1057
+ id: row["id"],
1058
+ resource_type: row["resource_type"],
1059
+ resource_id: row["resource_id"],
1060
+ agent_id: row["agent_id"],
1061
+ lock_type: row["lock_type"],
1062
+ locked_at: row["locked_at"],
1063
+ expires_at: row["expires_at"]
1064
+ };
1193
1065
  }
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 {};
1066
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
1067
+ const d = db || getDatabase();
1068
+ cleanExpiredLocks(d);
1069
+ 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);
1070
+ if (ownLock) {
1071
+ const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1072
+ d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
1073
+ newExpiry,
1074
+ ownLock["id"]
1075
+ ]);
1076
+ return parseLockRow({ ...ownLock, expires_at: newExpiry });
1202
1077
  }
1078
+ if (lockType === "exclusive") {
1079
+ 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);
1080
+ if (existing) {
1081
+ return null;
1082
+ }
1083
+ }
1084
+ const id = shortUuid();
1085
+ const lockedAt = now();
1086
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
1087
+ 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]);
1088
+ return {
1089
+ id,
1090
+ resource_type: resourceType,
1091
+ resource_id: resourceId,
1092
+ agent_id: agentId,
1093
+ lock_type: lockType,
1094
+ locked_at: lockedAt,
1095
+ expires_at: expiresAt
1096
+ };
1203
1097
  }
1204
- function writeGlobalConfig(data) {
1205
- const p = globalConfigPath();
1206
- ensureDir2(dirname2(p));
1207
- writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
1098
+ function releaseLock(lockId, agentId, db) {
1099
+ const d = db || getDatabase();
1100
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1101
+ return result.changes > 0;
1208
1102
  }
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;
1103
+ function releaseResourceLocks(agentId, resourceType, resourceId, db) {
1104
+ const d = db || getDatabase();
1105
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ?", [agentId, resourceType, resourceId]);
1106
+ return result.changes;
1215
1107
  }
1216
- function setActiveProfile(name) {
1217
- const cfg = readGlobalConfig();
1218
- if (name === null) {
1219
- delete cfg["active_profile"];
1220
- } else {
1221
- cfg["active_profile"] = name;
1222
- }
1223
- writeGlobalConfig(cfg);
1108
+ function releaseAllAgentLocks(agentId, db) {
1109
+ const d = db || getDatabase();
1110
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1111
+ return result.changes;
1224
1112
  }
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();
1113
+ function checkLock(resourceType, resourceId, lockType, db) {
1114
+ const d = db || getDatabase();
1115
+ cleanExpiredLocks(d);
1116
+ 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')";
1117
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
1118
+ return rows.map(parseLockRow);
1230
1119
  }
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;
1120
+ function agentHoldsLock(agentId, resourceType, resourceId, lockType, db) {
1121
+ const d = db || getDatabase();
1122
+ 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')";
1123
+ const row = lockType ? d.query(query).get(agentId, resourceType, resourceId, lockType) : d.query(query).get(agentId, resourceType, resourceId);
1124
+ return row ? parseLockRow(row) : null;
1239
1125
  }
1240
- function ensureDir2(dir) {
1241
- if (!existsSync2(dir)) {
1242
- mkdirSync2(dir, { recursive: true });
1126
+ function listAgentLocks(agentId, db) {
1127
+ const d = db || getDatabase();
1128
+ cleanExpiredLocks(d);
1129
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > datetime('now') ORDER BY locked_at DESC").all(agentId);
1130
+ return rows.map(parseLockRow);
1131
+ }
1132
+ function cleanExpiredLocks(db) {
1133
+ const d = db || getDatabase();
1134
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= datetime('now')");
1135
+ return result.changes;
1136
+ }
1137
+ // src/lib/memory-lock.ts
1138
+ var MEMORY_WRITE_TTL = 30;
1139
+ function memoryLockId(key, scope, projectId) {
1140
+ return `${scope}:${key}:${projectId ?? ""}`;
1141
+ }
1142
+ function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
1143
+ const d = db || getDatabase();
1144
+ return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
1145
+ }
1146
+ function releaseMemoryWriteLock(lockId, agentId, db) {
1147
+ const d = db || getDatabase();
1148
+ return releaseLock(lockId, agentId, d);
1149
+ }
1150
+ function checkMemoryWriteLock(key, scope, projectId, db) {
1151
+ const d = db || getDatabase();
1152
+ const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
1153
+ return locks[0] ?? null;
1154
+ }
1155
+ function withMemoryLock(agentId, key, scope, projectId, fn, ttlSeconds = MEMORY_WRITE_TTL, db) {
1156
+ const d = db || getDatabase();
1157
+ const lock = acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds, d);
1158
+ if (!lock) {
1159
+ const existing = checkMemoryWriteLock(key, scope, projectId, d);
1160
+ throw new MemoryLockConflictError(key, scope, existing?.agent_id ?? "unknown");
1161
+ }
1162
+ try {
1163
+ return fn();
1164
+ } finally {
1165
+ releaseLock(lock.id, agentId, d);
1243
1166
  }
1244
1167
  }
1245
1168
 
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
- }
1169
+ class MemoryLockConflictError extends Error {
1170
+ conflict = true;
1171
+ key;
1172
+ scope;
1173
+ blocking_agent_id;
1174
+ constructor(key, scope, blockingAgentId) {
1175
+ super(`Memory key "${key}" (scope: ${scope}) is currently write-locked by agent ${blockingAgentId}. ` + "Retry after a few seconds or use optimistic locking (version field).");
1176
+ this.name = "MemoryLockConflictError";
1177
+ this.key = key;
1178
+ this.scope = scope;
1179
+ this.blocking_agent_id = blockingAgentId;
1267
1180
  }
1268
1181
  }
1269
- function parseMemoryRow(row) {
1182
+ // src/lib/focus.ts
1183
+ var sessionFocus = new Map;
1184
+ function setFocus(agentId, projectId) {
1185
+ sessionFocus.set(agentId, projectId);
1186
+ updateAgent(agentId, { active_project_id: projectId });
1187
+ }
1188
+ function getFocus(agentId) {
1189
+ if (sessionFocus.has(agentId)) {
1190
+ return sessionFocus.get(agentId) ?? null;
1191
+ }
1192
+ const agent = getAgent(agentId);
1193
+ const projectId = agent?.active_project_id ?? null;
1194
+ sessionFocus.set(agentId, projectId);
1195
+ return projectId;
1196
+ }
1197
+ function unfocus(agentId) {
1198
+ setFocus(agentId, null);
1199
+ }
1200
+ function resolveProjectId(agentId, explicitProjectId) {
1201
+ if (explicitProjectId !== undefined && explicitProjectId !== null) {
1202
+ return explicitProjectId;
1203
+ }
1204
+ if (agentId) {
1205
+ return getFocus(agentId);
1206
+ }
1207
+ return null;
1208
+ }
1209
+ function buildFocusFilter(agentId, explicitProjectId, explicitScope) {
1210
+ if (explicitScope || explicitProjectId)
1211
+ return null;
1212
+ if (!agentId)
1213
+ return null;
1214
+ const focusedProjectId = getFocus(agentId);
1215
+ if (!focusedProjectId)
1216
+ return null;
1217
+ return {
1218
+ focusMode: true,
1219
+ agentId,
1220
+ projectId: focusedProjectId
1221
+ };
1222
+ }
1223
+ function focusFilterSQL(agentId, projectId) {
1224
+ return {
1225
+ sql: "(scope = 'global' OR (scope = 'private' AND agent_id = ?) OR (scope = 'shared' AND project_id = ?))",
1226
+ params: [agentId, projectId]
1227
+ };
1228
+ }
1229
+ // src/db/projects.ts
1230
+ function parseProjectRow(row) {
1270
1231
  return {
1271
1232
  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,
1233
+ name: row["name"],
1234
+ path: row["path"],
1235
+ description: row["description"] || null,
1236
+ memory_prefix: row["memory_prefix"] || null,
1289
1237
  created_at: row["created_at"],
1290
- updated_at: row["updated_at"],
1291
- accessed_at: row["accessed_at"] || null
1238
+ updated_at: row["updated_at"]
1292
1239
  };
1293
1240
  }
1294
- function createMemory(input, dedupeMode = "merge", db) {
1241
+ function registerProject(name, path, description, memoryPrefix, db) {
1295
1242
  const d = db || getDatabase();
1296
1243
  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();
1244
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
1245
+ if (existing) {
1246
+ const existingId = existing["id"];
1247
+ d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
1248
+ timestamp,
1249
+ existingId
1250
+ ]);
1251
+ return parseProjectRow(existing);
1300
1252
  }
1301
1253
  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;
1254
+ 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]);
1255
+ return getProject(id, d);
1256
+ }
1257
+ function getProject(idOrPath, db) {
1258
+ const d = db || getDatabase();
1259
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
1260
+ if (row)
1261
+ return parseProjectRow(row);
1262
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
1263
+ if (row)
1264
+ return parseProjectRow(row);
1265
+ row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
1266
+ if (row)
1267
+ return parseProjectRow(row);
1268
+ return null;
1269
+ }
1270
+ function listProjects(db) {
1271
+ const d = db || getDatabase();
1272
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
1273
+ return rows.map(parseProjectRow);
1274
+ }
1275
+ // src/db/entities.ts
1276
+ function parseEntityRow2(row) {
1277
+ return {
1278
+ id: row["id"],
1279
+ name: row["name"],
1280
+ type: row["type"],
1281
+ description: row["description"] || null,
1282
+ metadata: JSON.parse(row["metadata"] || "{}"),
1283
+ project_id: row["project_id"] || null,
1284
+ created_at: row["created_at"],
1285
+ updated_at: row["updated_at"]
1286
+ };
1287
+ }
1288
+ function createEntity(input, db) {
1289
+ const d = db || getDatabase();
1290
+ const timestamp = now();
1291
+ const metadataJson = JSON.stringify(input.metadata || {});
1292
+ const existing = d.query(`SELECT * FROM entities
1293
+ WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
1294
+ if (existing) {
1295
+ const sets = ["updated_at = ?"];
1296
+ const params = [timestamp];
1297
+ if (input.description !== undefined) {
1298
+ sets.push("description = ?");
1299
+ params.push(input.description);
1300
+ }
1301
+ if (input.metadata !== undefined) {
1302
+ sets.push("metadata = ?");
1303
+ params.push(metadataJson);
1344
1304
  }
1305
+ const existingId = existing["id"];
1306
+ params.push(existingId);
1307
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1308
+ return getEntity(existingId, d);
1345
1309
  }
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, ?, ?, ?)`, [
1310
+ const id = shortUuid();
1311
+ d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
1312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1348
1313
  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,
1314
+ input.name,
1315
+ input.type,
1316
+ input.description || null,
1360
1317
  metadataJson,
1361
- expiresAt,
1318
+ input.project_id || null,
1362
1319
  timestamp,
1363
1320
  timestamp
1364
1321
  ]);
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;
1322
+ return getEntity(id, d);
1374
1323
  }
1375
- function getMemory(id, db) {
1324
+ function getEntity(id, db) {
1376
1325
  const d = db || getDatabase();
1377
- const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
1326
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1378
1327
  if (!row)
1379
- return null;
1380
- return parseMemoryRow(row);
1328
+ throw new EntityNotFoundError(id);
1329
+ return parseEntityRow2(row);
1381
1330
  }
1382
- function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
1331
+ function getEntityByName(name, type, projectId, db) {
1383
1332
  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);
1333
+ let sql = "SELECT * FROM entities WHERE name = ?";
1334
+ const params = [name];
1335
+ if (type) {
1336
+ sql += " AND type = ?";
1337
+ params.push(type);
1393
1338
  }
1394
- if (projectId) {
1339
+ if (projectId !== undefined) {
1395
1340
  sql += " AND project_id = ?";
1396
1341
  params.push(projectId);
1397
1342
  }
1398
- if (sessionId) {
1399
- sql += " AND session_id = ?";
1400
- params.push(sessionId);
1401
- }
1402
- sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
1343
+ sql += " LIMIT 1";
1403
1344
  const row = d.query(sql).get(...params);
1404
1345
  if (!row)
1405
1346
  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);
1347
+ return parseEntityRow2(row);
1427
1348
  }
1428
- function listMemories(filter, db) {
1349
+ function listEntities(filter = {}, db) {
1429
1350
  const d = db || getDatabase();
1430
1351
  const conditions = [];
1431
1352
  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'");
1353
+ if (filter.type) {
1354
+ conditions.push("type = ?");
1355
+ params.push(filter.type);
1504
1356
  }
1505
- let sql = "SELECT * FROM memories";
1357
+ if (filter.project_id) {
1358
+ conditions.push("project_id = ?");
1359
+ params.push(filter.project_id);
1360
+ }
1361
+ if (filter.search) {
1362
+ conditions.push("(name LIKE ? OR description LIKE ?)");
1363
+ const term = `%${filter.search}%`;
1364
+ params.push(term, term);
1365
+ }
1366
+ let sql = "SELECT * FROM entities";
1506
1367
  if (conditions.length > 0) {
1507
1368
  sql += ` WHERE ${conditions.join(" AND ")}`;
1508
1369
  }
1509
- sql += " ORDER BY importance DESC, created_at DESC";
1510
- if (filter?.limit) {
1370
+ sql += " ORDER BY updated_at DESC";
1371
+ if (filter.limit) {
1511
1372
  sql += " LIMIT ?";
1512
1373
  params.push(filter.limit);
1513
1374
  }
1514
- if (filter?.offset) {
1375
+ if (filter.offset) {
1515
1376
  sql += " OFFSET ?";
1516
1377
  params.push(filter.offset);
1517
1378
  }
1518
1379
  const rows = d.query(sql).all(...params);
1519
- return rows.map(parseMemoryRow);
1380
+ return rows.map(parseEntityRow2);
1520
1381
  }
1521
- function updateMemory(id, input, db) {
1382
+ function updateEntity(id, input, db) {
1522
1383
  const d = db || getDatabase();
1523
- const existing = getMemory(id, d);
1384
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1524
1385
  if (!existing)
1525
- throw new MemoryNotFoundError(id);
1526
- if (existing.version !== input.version) {
1527
- throw new VersionConflictError(id, input.version, existing.version);
1386
+ throw new EntityNotFoundError(id);
1387
+ const sets = ["updated_at = ?"];
1388
+ const params = [now()];
1389
+ if (input.name !== undefined) {
1390
+ sets.push("name = ?");
1391
+ params.push(input.name);
1528
1392
  }
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 = ?"];
1547
- 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);
1567
- }
1568
- if (input.pinned !== undefined) {
1569
- sets.push("pinned = ?");
1570
- params.push(input.pinned ? 1 : 0);
1393
+ if (input.type !== undefined) {
1394
+ sets.push("type = ?");
1395
+ params.push(input.type);
1571
1396
  }
1572
- if (input.status !== undefined) {
1573
- sets.push("status = ?");
1574
- params.push(input.status);
1397
+ if (input.description !== undefined) {
1398
+ sets.push("description = ?");
1399
+ params.push(input.description);
1575
1400
  }
1576
1401
  if (input.metadata !== undefined) {
1577
1402
  sets.push("metadata = ?");
1578
1403
  params.push(JSON.stringify(input.metadata));
1579
1404
  }
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
1405
  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]);
1406
+ d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
1407
+ return getEntity(id, d);
1627
1408
  }
1628
- function cleanExpiredMemories(db) {
1409
+ function deleteEntity(id, db) {
1629
1410
  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;
1411
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1412
+ if (result.changes === 0)
1413
+ throw new EntityNotFoundError(id);
1637
1414
  }
1638
- function getMemoryVersions(memoryId, db) {
1415
+ function mergeEntities(sourceId, targetId, db) {
1639
1416
  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
- }
1417
+ getEntity(sourceId, d);
1418
+ getEntity(targetId, d);
1419
+ d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
1420
+ d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
1421
+ d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
1422
+ sourceId,
1423
+ sourceId
1424
+ ]);
1425
+ d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
1426
+ d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
1427
+ d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
1428
+ d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
1429
+ return getEntity(targetId, d);
1659
1430
  }
1660
- // src/db/locks.ts
1661
- function parseLockRow(row) {
1431
+
1432
+ // src/lib/search.ts
1433
+ function parseMemoryRow2(row) {
1662
1434
  return {
1663
1435
  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
1436
+ key: row["key"],
1437
+ value: row["value"],
1438
+ category: row["category"],
1439
+ scope: row["scope"],
1440
+ summary: row["summary"] || null,
1441
+ tags: JSON.parse(row["tags"] || "[]"),
1442
+ importance: row["importance"],
1443
+ source: row["source"],
1444
+ status: row["status"],
1445
+ pinned: !!row["pinned"],
1446
+ agent_id: row["agent_id"] || null,
1447
+ project_id: row["project_id"] || null,
1448
+ session_id: row["session_id"] || null,
1449
+ metadata: JSON.parse(row["metadata"] || "{}"),
1450
+ access_count: row["access_count"],
1451
+ version: row["version"],
1452
+ expires_at: row["expires_at"] || null,
1453
+ created_at: row["created_at"],
1454
+ updated_at: row["updated_at"],
1455
+ accessed_at: row["accessed_at"] || null
1702
1456
  };
1703
1457
  }
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;
1718
- }
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;
1458
+ function preprocessQuery(query) {
1459
+ let q = query.trim();
1460
+ q = q.replace(/\s+/g, " ");
1461
+ q = q.normalize("NFC");
1462
+ return q;
1742
1463
  }
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, "\\_");
1464
+ function escapeLikePattern(s) {
1465
+ return s.replace(/%/g, "\\%").replace(/_/g, "\\_");
1822
1466
  }
1823
1467
  var STOP_WORDS = new Set([
1824
1468
  "a",
@@ -2309,6 +1953,12 @@ function searchMemories(query, filter, db) {
2309
1953
  const offset = filter?.offset ?? 0;
2310
1954
  const limit = filter?.limit ?? scored.length;
2311
1955
  const finalResults = scored.slice(offset, offset + limit);
1956
+ if (finalResults.length > 0 && scored.length > 0) {
1957
+ const topScore = scored[0]?.score ?? 0;
1958
+ const secondScore = scored[1]?.score ?? 0;
1959
+ const confidence = topScore > 0 ? Math.max(0, Math.min(1, (topScore - secondScore) / topScore)) : 0;
1960
+ finalResults[0] = { ...finalResults[0], confidence };
1961
+ }
2312
1962
  logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
2313
1963
  return finalResults;
2314
1964
  }
@@ -2319,6 +1969,148 @@ function logSearchQuery(query, resultCount, agentId, projectId, db) {
2319
1969
  d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
2320
1970
  } catch {}
2321
1971
  }
1972
+ // src/lib/config.ts
1973
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1974
+ import { homedir } from "os";
1975
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1976
+ var DEFAULT_CONFIG = {
1977
+ default_scope: "private",
1978
+ default_category: "knowledge",
1979
+ default_importance: 5,
1980
+ max_entries: 1000,
1981
+ max_entries_per_scope: {
1982
+ global: 500,
1983
+ shared: 300,
1984
+ private: 200
1985
+ },
1986
+ injection: {
1987
+ max_tokens: 500,
1988
+ min_importance: 5,
1989
+ categories: ["preference", "fact"],
1990
+ refresh_interval: 5
1991
+ },
1992
+ extraction: {
1993
+ enabled: true,
1994
+ min_confidence: 0.5
1995
+ },
1996
+ sync_agents: ["claude", "codex", "gemini"],
1997
+ auto_cleanup: {
1998
+ enabled: true,
1999
+ expired_check_interval: 3600,
2000
+ unused_archive_days: 7,
2001
+ stale_deprioritize_days: 14
2002
+ }
2003
+ };
2004
+ function deepMerge(target, source) {
2005
+ const result = { ...target };
2006
+ for (const key of Object.keys(source)) {
2007
+ const sourceVal = source[key];
2008
+ const targetVal = result[key];
2009
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
2010
+ result[key] = deepMerge(targetVal, sourceVal);
2011
+ } else {
2012
+ result[key] = sourceVal;
2013
+ }
2014
+ }
2015
+ return result;
2016
+ }
2017
+ var VALID_SCOPES = ["global", "shared", "private"];
2018
+ var VALID_CATEGORIES = [
2019
+ "preference",
2020
+ "fact",
2021
+ "knowledge",
2022
+ "history"
2023
+ ];
2024
+ function isValidScope(value) {
2025
+ return VALID_SCOPES.includes(value);
2026
+ }
2027
+ function isValidCategory(value) {
2028
+ return VALID_CATEGORIES.includes(value);
2029
+ }
2030
+ function loadConfig() {
2031
+ const configPath = join2(homedir(), ".mementos", "config.json");
2032
+ let fileConfig = {};
2033
+ if (existsSync2(configPath)) {
2034
+ try {
2035
+ const raw = readFileSync(configPath, "utf-8");
2036
+ fileConfig = JSON.parse(raw);
2037
+ } catch {}
2038
+ }
2039
+ const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
2040
+ const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
2041
+ if (envScope && isValidScope(envScope)) {
2042
+ merged.default_scope = envScope;
2043
+ }
2044
+ const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
2045
+ if (envCategory && isValidCategory(envCategory)) {
2046
+ merged.default_category = envCategory;
2047
+ }
2048
+ const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
2049
+ if (envImportance) {
2050
+ const parsed = parseInt(envImportance, 10);
2051
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
2052
+ merged.default_importance = parsed;
2053
+ }
2054
+ }
2055
+ return merged;
2056
+ }
2057
+ function profilesDir() {
2058
+ return join2(homedir(), ".mementos", "profiles");
2059
+ }
2060
+ function globalConfigPath() {
2061
+ return join2(homedir(), ".mementos", "config.json");
2062
+ }
2063
+ function readGlobalConfig() {
2064
+ const p = globalConfigPath();
2065
+ if (!existsSync2(p))
2066
+ return {};
2067
+ try {
2068
+ return JSON.parse(readFileSync(p, "utf-8"));
2069
+ } catch {
2070
+ return {};
2071
+ }
2072
+ }
2073
+ function writeGlobalConfig(data) {
2074
+ const p = globalConfigPath();
2075
+ ensureDir2(dirname2(p));
2076
+ writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
2077
+ }
2078
+ function getActiveProfile() {
2079
+ const envProfile = process.env["MEMENTOS_PROFILE"];
2080
+ if (envProfile)
2081
+ return envProfile.trim();
2082
+ const cfg = readGlobalConfig();
2083
+ return cfg["active_profile"] || null;
2084
+ }
2085
+ function setActiveProfile(name) {
2086
+ const cfg = readGlobalConfig();
2087
+ if (name === null) {
2088
+ delete cfg["active_profile"];
2089
+ } else {
2090
+ cfg["active_profile"] = name;
2091
+ }
2092
+ writeGlobalConfig(cfg);
2093
+ }
2094
+ function listProfiles() {
2095
+ const dir = profilesDir();
2096
+ if (!existsSync2(dir))
2097
+ return [];
2098
+ return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
2099
+ }
2100
+ function deleteProfile(name) {
2101
+ const dbPath = join2(profilesDir(), `${name}.db`);
2102
+ if (!existsSync2(dbPath))
2103
+ return false;
2104
+ unlinkSync(dbPath);
2105
+ if (getActiveProfile() === name)
2106
+ setActiveProfile(null);
2107
+ return true;
2108
+ }
2109
+ function ensureDir2(dir) {
2110
+ if (!existsSync2(dir)) {
2111
+ mkdirSync2(dir, { recursive: true });
2112
+ }
2113
+ }
2322
2114
  // src/lib/injector.ts
2323
2115
  class MemoryInjector {
2324
2116
  config;
@@ -2486,183 +2278,1084 @@ function enforceQuotas(config, db) {
2486
2278
  d.run(`DELETE FROM memories WHERE id IN (${subquery})`, [scope, excess]);
2487
2279
  totalEvicted += delCount;
2488
2280
  }
2489
- return totalEvicted;
2281
+ return totalEvicted;
2282
+ }
2283
+ function archiveStale(staleDays, db) {
2284
+ const d = db || getDatabase();
2285
+ const timestamp = now();
2286
+ const cutoff = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000).toISOString();
2287
+ const archiveWhere = `status = 'active' AND pinned = 0 AND COALESCE(accessed_at, created_at) < ?`;
2288
+ const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${archiveWhere}`).get(cutoff).c;
2289
+ if (count > 0) {
2290
+ d.run(`UPDATE memories SET status = 'archived', updated_at = ? WHERE ${archiveWhere}`, [timestamp, cutoff]);
2291
+ }
2292
+ return count;
2293
+ }
2294
+ function archiveUnused(days, db) {
2295
+ const d = db || getDatabase();
2296
+ const timestamp = now();
2297
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2298
+ const unusedWhere = `status = 'active' AND pinned = 0 AND access_count = 0 AND created_at < ?`;
2299
+ const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${unusedWhere}`).get(cutoff).c;
2300
+ if (count > 0) {
2301
+ d.run(`UPDATE memories SET status = 'archived', updated_at = ? WHERE ${unusedWhere}`, [timestamp, cutoff]);
2302
+ }
2303
+ return count;
2304
+ }
2305
+ function deprioritizeStale(days, db) {
2306
+ const d = db || getDatabase();
2307
+ const timestamp = now();
2308
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
2309
+ const deprioWhere = `status = 'active' AND pinned = 0 AND importance > 1 AND COALESCE(accessed_at, updated_at) < ?`;
2310
+ const count = d.query(`SELECT COUNT(*) as c FROM memories WHERE ${deprioWhere}`).get(cutoff).c;
2311
+ if (count > 0) {
2312
+ d.run(`UPDATE memories
2313
+ SET importance = MAX(importance - 1, 1),
2314
+ version = version + 1,
2315
+ updated_at = ?
2316
+ WHERE ${deprioWhere}`, [timestamp, cutoff]);
2317
+ }
2318
+ return count;
2319
+ }
2320
+ function runCleanup(config, db) {
2321
+ const d = db || getDatabase();
2322
+ const expired = cleanExpiredMemories(d);
2323
+ const evicted = enforceQuotas(config, d);
2324
+ const archived = archiveStale(90, d);
2325
+ const unused_archived = archiveUnused(config.auto_cleanup.unused_archive_days ?? 7, d);
2326
+ const deprioritized = deprioritizeStale(config.auto_cleanup.stale_deprioritize_days ?? 14, d);
2327
+ return { expired, evicted, archived, unused_archived, deprioritized };
2328
+ }
2329
+ // src/lib/sync.ts
2330
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2331
+ import { homedir as homedir2 } from "os";
2332
+ import { join as join3 } from "path";
2333
+ function getAgentSyncDir(agentName) {
2334
+ const dir = join3(homedir2(), ".mementos", "agents", agentName);
2335
+ if (!existsSync3(dir)) {
2336
+ mkdirSync3(dir, { recursive: true });
2337
+ }
2338
+ return dir;
2339
+ }
2340
+ function setHighWaterMark(agentDir, timestamp) {
2341
+ const markFile = join3(agentDir, ".highwatermark");
2342
+ writeFileSync2(markFile, timestamp, "utf-8");
2343
+ }
2344
+ function resolveConflict(local, remote, resolution) {
2345
+ switch (resolution) {
2346
+ case "prefer-local":
2347
+ return "local";
2348
+ case "prefer-remote":
2349
+ return "remote";
2350
+ case "prefer-newer":
2351
+ return new Date(local.updated_at).getTime() >= new Date(remote.updated_at).getTime() ? "local" : "remote";
2352
+ }
2353
+ }
2354
+ function pushMemories(agentName, agentId, projectId, db) {
2355
+ const agentDir = getAgentSyncDir(agentName);
2356
+ const memories = listMemories({
2357
+ agent_id: agentId,
2358
+ project_id: projectId,
2359
+ status: "active",
2360
+ limit: 1e4
2361
+ }, db);
2362
+ const outFile = join3(agentDir, "memories.json");
2363
+ writeFileSync2(outFile, JSON.stringify(memories, null, 2), "utf-8");
2364
+ if (memories.length > 0) {
2365
+ const latest = memories.reduce((a, b) => new Date(a.updated_at).getTime() > new Date(b.updated_at).getTime() ? a : b);
2366
+ setHighWaterMark(agentDir, latest.updated_at);
2367
+ }
2368
+ return memories.length;
2369
+ }
2370
+ function pullMemories(agentName, conflictResolution = "prefer-newer", db) {
2371
+ const agentDir = getAgentSyncDir(agentName);
2372
+ const inFile = join3(agentDir, "memories.json");
2373
+ if (!existsSync3(inFile)) {
2374
+ return { pulled: 0, conflicts: 0 };
2375
+ }
2376
+ const raw = readFileSync2(inFile, "utf-8");
2377
+ let remoteMemories;
2378
+ try {
2379
+ remoteMemories = JSON.parse(raw);
2380
+ } catch {
2381
+ return { pulled: 0, conflicts: 0 };
2382
+ }
2383
+ let pulled = 0;
2384
+ let conflicts = 0;
2385
+ for (const remote of remoteMemories) {
2386
+ const localMemories = listMemories({
2387
+ search: remote.key,
2388
+ scope: remote.scope,
2389
+ agent_id: remote.agent_id || undefined,
2390
+ project_id: remote.project_id || undefined,
2391
+ limit: 1
2392
+ }, db);
2393
+ const local = localMemories.find((m) => m.key === remote.key);
2394
+ if (local) {
2395
+ const winner = resolveConflict(local, remote, conflictResolution);
2396
+ if (winner === "remote") {
2397
+ createMemory({
2398
+ key: remote.key,
2399
+ value: remote.value,
2400
+ category: remote.category,
2401
+ scope: remote.scope,
2402
+ summary: remote.summary || undefined,
2403
+ tags: remote.tags,
2404
+ importance: remote.importance,
2405
+ source: remote.source,
2406
+ agent_id: remote.agent_id || undefined,
2407
+ project_id: remote.project_id || undefined,
2408
+ session_id: remote.session_id || undefined,
2409
+ metadata: remote.metadata,
2410
+ expires_at: remote.expires_at || undefined
2411
+ }, "merge", db);
2412
+ pulled++;
2413
+ }
2414
+ conflicts++;
2415
+ } else {
2416
+ createMemory({
2417
+ key: remote.key,
2418
+ value: remote.value,
2419
+ category: remote.category,
2420
+ scope: remote.scope,
2421
+ summary: remote.summary || undefined,
2422
+ tags: remote.tags,
2423
+ importance: remote.importance,
2424
+ source: remote.source,
2425
+ agent_id: remote.agent_id || undefined,
2426
+ project_id: remote.project_id || undefined,
2427
+ session_id: remote.session_id || undefined,
2428
+ metadata: remote.metadata,
2429
+ expires_at: remote.expires_at || undefined
2430
+ }, "create", db);
2431
+ pulled++;
2432
+ }
2433
+ }
2434
+ return { pulled, conflicts };
2435
+ }
2436
+ function syncMemories(agentName, direction = "both", options = {}) {
2437
+ const result = {
2438
+ pushed: 0,
2439
+ pulled: 0,
2440
+ conflicts: 0,
2441
+ errors: []
2442
+ };
2443
+ try {
2444
+ if (direction === "push" || direction === "both") {
2445
+ result.pushed = pushMemories(agentName, options.agent_id, options.project_id, options.db);
2446
+ }
2447
+ if (direction === "pull" || direction === "both") {
2448
+ const pullResult = pullMemories(agentName, options.conflict_resolution || "prefer-newer", options.db);
2449
+ result.pulled = pullResult.pulled;
2450
+ result.conflicts = pullResult.conflicts;
2451
+ }
2452
+ } catch (e) {
2453
+ result.errors.push(e instanceof Error ? e.message : String(e));
2454
+ }
2455
+ return result;
2456
+ }
2457
+ var defaultSyncAgents = ["claude", "codex", "gemini"];
2458
+ // src/db/relations.ts
2459
+ function parseRelationRow(row) {
2460
+ return {
2461
+ id: row["id"],
2462
+ source_entity_id: row["source_entity_id"],
2463
+ target_entity_id: row["target_entity_id"],
2464
+ relation_type: row["relation_type"],
2465
+ weight: row["weight"],
2466
+ metadata: JSON.parse(row["metadata"] || "{}"),
2467
+ created_at: row["created_at"]
2468
+ };
2469
+ }
2470
+ function parseEntityRow3(row) {
2471
+ return {
2472
+ id: row["id"],
2473
+ name: row["name"],
2474
+ type: row["type"],
2475
+ description: row["description"] || null,
2476
+ metadata: JSON.parse(row["metadata"] || "{}"),
2477
+ project_id: row["project_id"] || null,
2478
+ created_at: row["created_at"],
2479
+ updated_at: row["updated_at"]
2480
+ };
2481
+ }
2482
+ function createRelation(input, db) {
2483
+ const d = db || getDatabase();
2484
+ const id = shortUuid();
2485
+ const timestamp = now();
2486
+ const weight = input.weight ?? 1;
2487
+ const metadata = JSON.stringify(input.metadata ?? {});
2488
+ d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
2489
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2490
+ ON CONFLICT(source_entity_id, target_entity_id, relation_type)
2491
+ DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
2492
+ const row = d.query(`SELECT * FROM relations
2493
+ WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
2494
+ return parseRelationRow(row);
2495
+ }
2496
+ function getRelation(id, db) {
2497
+ const d = db || getDatabase();
2498
+ const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
2499
+ if (!row)
2500
+ throw new Error(`Relation not found: ${id}`);
2501
+ return parseRelationRow(row);
2502
+ }
2503
+ function listRelations(filter, db) {
2504
+ const d = db || getDatabase();
2505
+ const conditions = [];
2506
+ const params = [];
2507
+ if (filter.entity_id) {
2508
+ const dir = filter.direction || "both";
2509
+ if (dir === "outgoing") {
2510
+ conditions.push("source_entity_id = ?");
2511
+ params.push(filter.entity_id);
2512
+ } else if (dir === "incoming") {
2513
+ conditions.push("target_entity_id = ?");
2514
+ params.push(filter.entity_id);
2515
+ } else {
2516
+ conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
2517
+ params.push(filter.entity_id, filter.entity_id);
2518
+ }
2519
+ }
2520
+ if (filter.relation_type) {
2521
+ conditions.push("relation_type = ?");
2522
+ params.push(filter.relation_type);
2523
+ }
2524
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2525
+ const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
2526
+ return rows.map(parseRelationRow);
2527
+ }
2528
+ function deleteRelation(id, db) {
2529
+ const d = db || getDatabase();
2530
+ const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
2531
+ if (result.changes === 0)
2532
+ throw new Error(`Relation not found: ${id}`);
2533
+ }
2534
+ function getRelatedEntities(entityId, relationType, db) {
2535
+ const d = db || getDatabase();
2536
+ let sql;
2537
+ const params = [];
2538
+ if (relationType) {
2539
+ sql = `
2540
+ SELECT DISTINCT e.* FROM entities e
2541
+ JOIN relations r ON (
2542
+ (r.source_entity_id = ? AND r.target_entity_id = e.id)
2543
+ OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
2544
+ )
2545
+ WHERE r.relation_type = ?
2546
+ `;
2547
+ params.push(entityId, entityId, relationType);
2548
+ } else {
2549
+ sql = `
2550
+ SELECT DISTINCT e.* FROM entities e
2551
+ JOIN relations r ON (
2552
+ (r.source_entity_id = ? AND r.target_entity_id = e.id)
2553
+ OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
2554
+ )
2555
+ `;
2556
+ params.push(entityId, entityId);
2557
+ }
2558
+ const rows = d.query(sql).all(...params);
2559
+ return rows.map(parseEntityRow3);
2560
+ }
2561
+ function getEntityGraph(entityId, depth = 2, db) {
2562
+ const d = db || getDatabase();
2563
+ const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
2564
+ VALUES(?, 0)
2565
+ UNION
2566
+ SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
2567
+ FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
2568
+ WHERE g.depth < ?
2569
+ )
2570
+ SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
2571
+ const entities = entityRows.map(parseEntityRow3);
2572
+ const entityIds = new Set(entities.map((e) => e.id));
2573
+ if (entityIds.size === 0) {
2574
+ return { entities: [], relations: [] };
2575
+ }
2576
+ const placeholders = Array.from(entityIds).map(() => "?").join(",");
2577
+ const relationRows = d.query(`SELECT * FROM relations
2578
+ WHERE source_entity_id IN (${placeholders})
2579
+ AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
2580
+ const relations = relationRows.map(parseRelationRow);
2581
+ return { entities, relations };
2582
+ }
2583
+ function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
2584
+ const d = db || getDatabase();
2585
+ const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
2586
+ SELECT ?, ?, 0
2587
+ UNION
2588
+ SELECT
2589
+ CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
2590
+ p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
2591
+ p.depth + 1
2592
+ FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
2593
+ WHERE p.depth < ?
2594
+ AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
2595
+ )
2596
+ SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
2597
+ if (!rows)
2598
+ return null;
2599
+ const ids = rows.trail.split(",");
2600
+ const entities = [];
2601
+ for (const id of ids) {
2602
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
2603
+ if (row)
2604
+ entities.push(parseEntityRow3(row));
2605
+ }
2606
+ return entities.length > 0 ? entities : null;
2607
+ }
2608
+ // src/lib/providers/base.ts
2609
+ var DEFAULT_AUTO_MEMORY_CONFIG = {
2610
+ provider: "anthropic",
2611
+ model: "claude-haiku-4-5",
2612
+ enabled: true,
2613
+ minImportance: 4,
2614
+ autoEntityLink: true,
2615
+ fallback: ["cerebras", "openai"]
2616
+ };
2617
+
2618
+ class BaseProvider {
2619
+ config;
2620
+ constructor(config) {
2621
+ this.config = config;
2622
+ }
2623
+ parseJSON(raw) {
2624
+ try {
2625
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
2626
+ return JSON.parse(cleaned);
2627
+ } catch {
2628
+ return null;
2629
+ }
2630
+ }
2631
+ clampImportance(value) {
2632
+ const n = Number(value);
2633
+ if (isNaN(n))
2634
+ return 5;
2635
+ return Math.max(0, Math.min(10, Math.round(n)));
2636
+ }
2637
+ normaliseMemory(raw) {
2638
+ if (!raw || typeof raw !== "object")
2639
+ return null;
2640
+ const m = raw;
2641
+ if (typeof m.content !== "string" || !m.content.trim())
2642
+ return null;
2643
+ const validScopes = ["private", "shared", "global"];
2644
+ const validCategories = [
2645
+ "preference",
2646
+ "fact",
2647
+ "knowledge",
2648
+ "history"
2649
+ ];
2650
+ return {
2651
+ content: m.content.trim(),
2652
+ category: validCategories.includes(m.category) ? m.category : "knowledge",
2653
+ importance: this.clampImportance(m.importance),
2654
+ tags: Array.isArray(m.tags) ? m.tags.filter((t) => typeof t === "string").map((t) => t.toLowerCase()) : [],
2655
+ suggestedScope: validScopes.includes(m.suggestedScope) ? m.suggestedScope : "shared",
2656
+ reasoning: typeof m.reasoning === "string" ? m.reasoning : undefined
2657
+ };
2658
+ }
2659
+ }
2660
+ var MEMORY_EXTRACTION_SYSTEM_PROMPT = `You are a precise memory extraction engine for an AI agent.
2661
+ Given text, extract facts worth remembering as structured JSON.
2662
+ Focus on: decisions made, preferences revealed, corrections, architectural choices, established facts, user preferences.
2663
+ Ignore: greetings, filler, questions without answers, temporary states.
2664
+ Output ONLY a JSON array \u2014 no markdown, no explanation.`;
2665
+ var MEMORY_EXTRACTION_USER_TEMPLATE = (text, context) => `Extract memories from this text.
2666
+ ${context.projectName ? `Project: ${context.projectName}` : ""}
2667
+ ${context.existingMemoriesSummary ? `Existing memories (avoid duplicates):
2668
+ ${context.existingMemoriesSummary}` : ""}
2669
+
2670
+ Text:
2671
+ ${text}
2672
+
2673
+ Return a JSON array of objects with these exact fields:
2674
+ - content: string (the memory, concise and specific)
2675
+ - category: "preference" | "fact" | "knowledge" | "history"
2676
+ - importance: number 0-10 (10 = critical, 0 = trivial)
2677
+ - tags: string[] (lowercase keywords)
2678
+ - suggestedScope: "private" | "shared" | "global"
2679
+ - reasoning: string (one sentence why this is worth remembering)
2680
+
2681
+ Return [] if nothing is worth remembering.`;
2682
+ var ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are a knowledge graph entity extractor.
2683
+ Given text, identify named entities and their relationships.
2684
+ Output ONLY valid JSON \u2014 no markdown, no explanation.`;
2685
+ var ENTITY_EXTRACTION_USER_TEMPLATE = (text) => `Extract entities and relations from this text.
2686
+
2687
+ Text: ${text}
2688
+
2689
+ Return JSON with this exact shape:
2690
+ {
2691
+ "entities": [
2692
+ { "name": string, "type": "person"|"project"|"tool"|"concept"|"file"|"api"|"pattern"|"organization", "confidence": 0-1 }
2693
+ ],
2694
+ "relations": [
2695
+ { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements" }
2696
+ ]
2697
+ }`;
2698
+
2699
+ // src/lib/providers/anthropic.ts
2700
+ var ANTHROPIC_MODELS = {
2701
+ default: "claude-haiku-4-5",
2702
+ premium: "claude-sonnet-4-5"
2703
+ };
2704
+
2705
+ class AnthropicProvider extends BaseProvider {
2706
+ name = "anthropic";
2707
+ baseUrl = "https://api.anthropic.com/v1";
2708
+ constructor(config) {
2709
+ const apiKey = config?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
2710
+ super({
2711
+ apiKey,
2712
+ model: config?.model ?? ANTHROPIC_MODELS.default,
2713
+ maxTokens: config?.maxTokens ?? 1024,
2714
+ temperature: config?.temperature ?? 0,
2715
+ timeoutMs: config?.timeoutMs ?? 15000
2716
+ });
2717
+ }
2718
+ async extractMemories(text, context) {
2719
+ if (!this.config.apiKey)
2720
+ return [];
2721
+ try {
2722
+ const response = await this.callAPI(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2723
+ const parsed = this.parseJSON(response);
2724
+ if (!Array.isArray(parsed))
2725
+ return [];
2726
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2727
+ } catch (err) {
2728
+ console.error("[anthropic] extractMemories failed:", err);
2729
+ return [];
2730
+ }
2731
+ }
2732
+ async extractEntities(text) {
2733
+ const empty = { entities: [], relations: [] };
2734
+ if (!this.config.apiKey)
2735
+ return empty;
2736
+ try {
2737
+ const response = await this.callAPI(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2738
+ const parsed = this.parseJSON(response);
2739
+ if (!parsed || typeof parsed !== "object")
2740
+ return empty;
2741
+ return {
2742
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2743
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2744
+ };
2745
+ } catch (err) {
2746
+ console.error("[anthropic] extractEntities failed:", err);
2747
+ return empty;
2748
+ }
2749
+ }
2750
+ async scoreImportance(content, _context) {
2751
+ if (!this.config.apiKey)
2752
+ return 5;
2753
+ try {
2754
+ 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?
2755
+
2756
+ "${content}"
2757
+
2758
+ Return only a number 0-10.`);
2759
+ return this.clampImportance(response.trim());
2760
+ } catch {
2761
+ return 5;
2762
+ }
2763
+ }
2764
+ async callAPI(systemPrompt, userMessage) {
2765
+ const controller = new AbortController;
2766
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2767
+ try {
2768
+ const res = await fetch(`${this.baseUrl}/messages`, {
2769
+ method: "POST",
2770
+ headers: {
2771
+ "Content-Type": "application/json",
2772
+ "x-api-key": this.config.apiKey,
2773
+ "anthropic-version": "2023-06-01"
2774
+ },
2775
+ body: JSON.stringify({
2776
+ model: this.config.model,
2777
+ max_tokens: this.config.maxTokens ?? 1024,
2778
+ temperature: this.config.temperature ?? 0,
2779
+ system: systemPrompt,
2780
+ messages: [{ role: "user", content: userMessage }]
2781
+ }),
2782
+ signal: controller.signal
2783
+ });
2784
+ if (!res.ok) {
2785
+ const body = await res.text().catch(() => "");
2786
+ throw new Error(`Anthropic API ${res.status}: ${body.slice(0, 200)}`);
2787
+ }
2788
+ const data = await res.json();
2789
+ return data.content?.[0]?.text ?? "";
2790
+ } finally {
2791
+ clearTimeout(timeout);
2792
+ }
2793
+ }
2794
+ }
2795
+
2796
+ // src/lib/providers/openai-compat.ts
2797
+ class OpenAICompatProvider extends BaseProvider {
2798
+ constructor(config) {
2799
+ super(config);
2800
+ }
2801
+ async extractMemories(text, context) {
2802
+ if (!this.config.apiKey)
2803
+ return [];
2804
+ try {
2805
+ const response = await this.callWithRetry(MEMORY_EXTRACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_USER_TEMPLATE(text, context));
2806
+ const parsed = this.parseJSON(response);
2807
+ if (!Array.isArray(parsed))
2808
+ return [];
2809
+ return parsed.map((item) => this.normaliseMemory(item)).filter((m) => m !== null);
2810
+ } catch (err) {
2811
+ console.error(`[${this.name}] extractMemories failed:`, err);
2812
+ return [];
2813
+ }
2814
+ }
2815
+ async extractEntities(text) {
2816
+ const empty = { entities: [], relations: [] };
2817
+ if (!this.config.apiKey)
2818
+ return empty;
2819
+ try {
2820
+ const response = await this.callWithRetry(ENTITY_EXTRACTION_SYSTEM_PROMPT, ENTITY_EXTRACTION_USER_TEMPLATE(text));
2821
+ const parsed = this.parseJSON(response);
2822
+ if (!parsed || typeof parsed !== "object")
2823
+ return empty;
2824
+ return {
2825
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
2826
+ relations: Array.isArray(parsed.relations) ? parsed.relations : []
2827
+ };
2828
+ } catch (err) {
2829
+ console.error(`[${this.name}] extractEntities failed:`, err);
2830
+ return empty;
2831
+ }
2832
+ }
2833
+ async scoreImportance(content, _context) {
2834
+ if (!this.config.apiKey)
2835
+ return 5;
2836
+ try {
2837
+ 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?
2838
+
2839
+ "${content}"
2840
+
2841
+ Return only a number 0-10.`);
2842
+ return this.clampImportance(response.trim());
2843
+ } catch {
2844
+ return 5;
2845
+ }
2846
+ }
2847
+ async callWithRetry(systemPrompt, userMessage, retries = 3) {
2848
+ let lastError = null;
2849
+ for (let attempt = 0;attempt < retries; attempt++) {
2850
+ try {
2851
+ return await this.callAPI(systemPrompt, userMessage);
2852
+ } catch (err) {
2853
+ lastError = err instanceof Error ? err : new Error(String(err));
2854
+ const isRateLimit = lastError.message.includes("429") || lastError.message.toLowerCase().includes("rate limit");
2855
+ if (!isRateLimit || attempt === retries - 1)
2856
+ throw lastError;
2857
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
2858
+ }
2859
+ }
2860
+ throw lastError ?? new Error("Unknown error");
2861
+ }
2862
+ async callAPI(systemPrompt, userMessage) {
2863
+ const controller = new AbortController;
2864
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
2865
+ try {
2866
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
2867
+ method: "POST",
2868
+ headers: {
2869
+ "Content-Type": "application/json",
2870
+ [this.authHeader]: `Bearer ${this.config.apiKey}`
2871
+ },
2872
+ body: JSON.stringify({
2873
+ model: this.config.model,
2874
+ max_tokens: this.config.maxTokens ?? 1024,
2875
+ temperature: this.config.temperature ?? 0,
2876
+ messages: [
2877
+ { role: "system", content: systemPrompt },
2878
+ { role: "user", content: userMessage }
2879
+ ]
2880
+ }),
2881
+ signal: controller.signal
2882
+ });
2883
+ if (!res.ok) {
2884
+ const body = await res.text().catch(() => "");
2885
+ throw new Error(`${this.name} API ${res.status}: ${body.slice(0, 200)}`);
2886
+ }
2887
+ const data = await res.json();
2888
+ return data.choices?.[0]?.message?.content ?? "";
2889
+ } finally {
2890
+ clearTimeout(timeout);
2891
+ }
2892
+ }
2490
2893
  }
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]);
2894
+
2895
+ // src/lib/providers/openai.ts
2896
+ var OPENAI_MODELS = {
2897
+ default: "gpt-4.1-nano",
2898
+ mini: "gpt-4.1-mini",
2899
+ full: "gpt-4.1"
2900
+ };
2901
+
2902
+ class OpenAIProvider extends OpenAICompatProvider {
2903
+ name = "openai";
2904
+ baseUrl = "https://api.openai.com/v1";
2905
+ authHeader = "Authorization";
2906
+ constructor(config) {
2907
+ super({
2908
+ apiKey: config?.apiKey ?? process.env.OPENAI_API_KEY ?? "",
2909
+ model: config?.model ?? OPENAI_MODELS.default,
2910
+ maxTokens: config?.maxTokens ?? 1024,
2911
+ temperature: config?.temperature ?? 0,
2912
+ timeoutMs: config?.timeoutMs ?? 15000
2913
+ });
2499
2914
  }
2500
- return count;
2501
2915
  }
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]);
2916
+
2917
+ // src/lib/providers/cerebras.ts
2918
+ var CEREBRAS_MODELS = {
2919
+ default: "llama-3.3-70b",
2920
+ fast: "llama3.1-8b"
2921
+ };
2922
+
2923
+ class CerebrasProvider extends OpenAICompatProvider {
2924
+ name = "cerebras";
2925
+ baseUrl = "https://api.cerebras.ai/v1";
2926
+ authHeader = "Authorization";
2927
+ constructor(config) {
2928
+ super({
2929
+ apiKey: config?.apiKey ?? process.env.CEREBRAS_API_KEY ?? "",
2930
+ model: config?.model ?? CEREBRAS_MODELS.default,
2931
+ maxTokens: config?.maxTokens ?? 1024,
2932
+ temperature: config?.temperature ?? 0,
2933
+ timeoutMs: config?.timeoutMs ?? 1e4
2934
+ });
2510
2935
  }
2511
- return count;
2512
2936
  }
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]);
2937
+
2938
+ // src/lib/providers/grok.ts
2939
+ var GROK_MODELS = {
2940
+ default: "grok-3-mini",
2941
+ premium: "grok-3"
2942
+ };
2943
+
2944
+ class GrokProvider extends OpenAICompatProvider {
2945
+ name = "grok";
2946
+ baseUrl = "https://api.x.ai/v1";
2947
+ authHeader = "Authorization";
2948
+ constructor(config) {
2949
+ super({
2950
+ apiKey: config?.apiKey ?? process.env.XAI_API_KEY ?? "",
2951
+ model: config?.model ?? GROK_MODELS.default,
2952
+ maxTokens: config?.maxTokens ?? 1024,
2953
+ temperature: config?.temperature ?? 0,
2954
+ timeoutMs: config?.timeoutMs ?? 15000
2955
+ });
2525
2956
  }
2526
- return count;
2527
2957
  }
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 };
2958
+
2959
+ // src/lib/providers/registry.ts
2960
+ class ProviderRegistry {
2961
+ config = { ...DEFAULT_AUTO_MEMORY_CONFIG };
2962
+ _instances = new Map;
2963
+ configure(partial) {
2964
+ this.config = { ...this.config, ...partial };
2965
+ this._instances.clear();
2966
+ }
2967
+ getConfig() {
2968
+ return this.config;
2969
+ }
2970
+ getPrimary() {
2971
+ return this.getProvider(this.config.provider);
2972
+ }
2973
+ getFallbacks() {
2974
+ const fallbackNames = this.config.fallback ?? [];
2975
+ return fallbackNames.filter((n) => n !== this.config.provider).map((n) => this.getProvider(n)).filter((p) => p !== null);
2976
+ }
2977
+ getAvailable() {
2978
+ const primary = this.getPrimary();
2979
+ if (primary)
2980
+ return primary;
2981
+ const fallbacks = this.getFallbacks();
2982
+ return fallbacks[0] ?? null;
2983
+ }
2984
+ getProvider(name) {
2985
+ const cached = this._instances.get(name);
2986
+ if (cached)
2987
+ return cached;
2988
+ const provider = this.createProvider(name);
2989
+ if (!provider)
2990
+ return null;
2991
+ if (!provider.config.apiKey)
2992
+ return null;
2993
+ this._instances.set(name, provider);
2994
+ return provider;
2995
+ }
2996
+ health() {
2997
+ const providers = ["anthropic", "openai", "cerebras", "grok"];
2998
+ const result = {};
2999
+ for (const name of providers) {
3000
+ const p = this.createProvider(name);
3001
+ result[name] = {
3002
+ available: Boolean(p?.config.apiKey),
3003
+ model: p?.config.model ?? "unknown"
3004
+ };
3005
+ }
3006
+ return result;
3007
+ }
3008
+ createProvider(name) {
3009
+ const modelOverride = name === this.config.provider ? this.config.model : undefined;
3010
+ switch (name) {
3011
+ case "anthropic":
3012
+ return new AnthropicProvider(modelOverride ? { model: modelOverride } : undefined);
3013
+ case "openai":
3014
+ return new OpenAIProvider(modelOverride ? { model: modelOverride } : undefined);
3015
+ case "cerebras":
3016
+ return new CerebrasProvider(modelOverride ? { model: modelOverride } : undefined);
3017
+ case "grok":
3018
+ return new GrokProvider(modelOverride ? { model: modelOverride } : undefined);
3019
+ default:
3020
+ return null;
3021
+ }
3022
+ }
2536
3023
  }
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 });
3024
+ var providerRegistry = new ProviderRegistry;
3025
+ function autoConfigureFromEnv() {
3026
+ const hasAnthropicKey = Boolean(process.env.ANTHROPIC_API_KEY);
3027
+ const hasCerebrasKey = Boolean(process.env.CEREBRAS_API_KEY);
3028
+ const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY);
3029
+ const hasGrokKey = Boolean(process.env.XAI_API_KEY);
3030
+ if (!hasAnthropicKey) {
3031
+ if (hasCerebrasKey) {
3032
+ providerRegistry.configure({ provider: "cerebras" });
3033
+ } else if (hasOpenAIKey) {
3034
+ providerRegistry.configure({ provider: "openai" });
3035
+ } else if (hasGrokKey) {
3036
+ providerRegistry.configure({ provider: "grok" });
3037
+ }
2545
3038
  }
2546
- return dir;
3039
+ const allProviders = ["anthropic", "cerebras", "openai", "grok"];
3040
+ const available = allProviders.filter((p) => {
3041
+ switch (p) {
3042
+ case "anthropic":
3043
+ return hasAnthropicKey;
3044
+ case "cerebras":
3045
+ return hasCerebrasKey;
3046
+ case "openai":
3047
+ return hasOpenAIKey;
3048
+ case "grok":
3049
+ return hasGrokKey;
3050
+ }
3051
+ });
3052
+ const primary = providerRegistry.getConfig().provider;
3053
+ const fallback = available.filter((p) => p !== primary);
3054
+ providerRegistry.configure({ fallback });
2547
3055
  }
2548
- function setHighWaterMark(agentDir, timestamp) {
2549
- const markFile = join3(agentDir, ".highwatermark");
2550
- writeFileSync2(markFile, timestamp, "utf-8");
3056
+ autoConfigureFromEnv();
3057
+
3058
+ // src/lib/auto-memory-queue.ts
3059
+ var MAX_QUEUE_SIZE = 100;
3060
+ var CONCURRENCY = 3;
3061
+
3062
+ class AutoMemoryQueue {
3063
+ queue = [];
3064
+ handler = null;
3065
+ running = false;
3066
+ activeCount = 0;
3067
+ stats = {
3068
+ pending: 0,
3069
+ processing: 0,
3070
+ processed: 0,
3071
+ failed: 0,
3072
+ dropped: 0
3073
+ };
3074
+ setHandler(handler) {
3075
+ this.handler = handler;
3076
+ if (!this.running)
3077
+ this.startLoop();
3078
+ }
3079
+ enqueue(job) {
3080
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
3081
+ this.queue.shift();
3082
+ this.stats.dropped++;
3083
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
3084
+ }
3085
+ this.queue.push(job);
3086
+ this.stats.pending++;
3087
+ if (!this.running && this.handler)
3088
+ this.startLoop();
3089
+ }
3090
+ getStats() {
3091
+ return { ...this.stats, pending: this.queue.length };
3092
+ }
3093
+ startLoop() {
3094
+ this.running = true;
3095
+ this.loop();
3096
+ }
3097
+ async loop() {
3098
+ while (this.queue.length > 0 || this.activeCount > 0) {
3099
+ while (this.queue.length > 0 && this.activeCount < CONCURRENCY) {
3100
+ const job = this.queue.shift();
3101
+ if (!job)
3102
+ break;
3103
+ this.stats.pending = Math.max(0, this.stats.pending - 1);
3104
+ this.activeCount++;
3105
+ this.stats.processing = this.activeCount;
3106
+ this.processJob(job);
3107
+ }
3108
+ await new Promise((r) => setImmediate(r));
3109
+ }
3110
+ this.running = false;
3111
+ }
3112
+ async processJob(job) {
3113
+ if (!this.handler) {
3114
+ this.activeCount--;
3115
+ this.stats.processing = this.activeCount;
3116
+ return;
3117
+ }
3118
+ try {
3119
+ await this.handler(job);
3120
+ this.stats.processed++;
3121
+ } catch (err) {
3122
+ this.stats.failed++;
3123
+ console.error("[auto-memory-queue] job failed:", err);
3124
+ } finally {
3125
+ this.activeCount--;
3126
+ this.stats.processing = this.activeCount;
3127
+ }
3128
+ }
2551
3129
  }
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";
3130
+ var autoMemoryQueue = new AutoMemoryQueue;
3131
+
3132
+ // src/lib/auto-memory.ts
3133
+ var DEDUP_SIMILARITY_THRESHOLD = 0.85;
3134
+ function isDuplicate(content, agentId, projectId) {
3135
+ try {
3136
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 10).join(" ");
3137
+ if (!query)
3138
+ return false;
3139
+ const results = searchMemories(query, {
3140
+ agent_id: agentId,
3141
+ project_id: projectId,
3142
+ limit: 3
3143
+ });
3144
+ if (results.length === 0)
3145
+ return false;
3146
+ const contentWords = new Set(content.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
3147
+ for (const result of results) {
3148
+ const existingWords = new Set(result.memory.value.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
3149
+ if (contentWords.size === 0 || existingWords.size === 0)
3150
+ continue;
3151
+ const intersection = [...contentWords].filter((w) => existingWords.has(w)).length;
3152
+ const union = new Set([...contentWords, ...existingWords]).size;
3153
+ const similarity = intersection / union;
3154
+ if (similarity >= DEDUP_SIMILARITY_THRESHOLD)
3155
+ return true;
3156
+ }
3157
+ return false;
3158
+ } catch {
3159
+ return false;
2560
3160
  }
2561
3161
  }
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);
3162
+ async function linkEntitiesToMemory(memoryId, content, _agentId, projectId) {
3163
+ const provider = providerRegistry.getAvailable();
3164
+ if (!provider)
3165
+ return;
3166
+ try {
3167
+ const { entities, relations } = await provider.extractEntities(content);
3168
+ const entityIdMap = new Map;
3169
+ for (const extracted of entities) {
3170
+ if (extracted.confidence < 0.6)
3171
+ continue;
3172
+ try {
3173
+ const existing = getEntityByName(extracted.name);
3174
+ const entityId = existing ? existing.id : createEntity({
3175
+ name: extracted.name,
3176
+ type: extracted.type,
3177
+ project_id: projectId
3178
+ }).id;
3179
+ entityIdMap.set(extracted.name, entityId);
3180
+ linkEntityToMemory(entityId, memoryId, "subject");
3181
+ } catch {}
3182
+ }
3183
+ for (const rel of relations) {
3184
+ const fromId = entityIdMap.get(rel.from);
3185
+ const toId = entityIdMap.get(rel.to);
3186
+ if (!fromId || !toId)
3187
+ continue;
3188
+ try {
3189
+ createRelation({
3190
+ source_entity_id: fromId,
3191
+ target_entity_id: toId,
3192
+ relation_type: rel.type
3193
+ });
3194
+ } catch {}
3195
+ }
3196
+ } catch (err) {
3197
+ console.error("[auto-memory] entity linking failed:", err);
2575
3198
  }
2576
- return memories.length;
2577
3199
  }
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 };
3200
+ async function saveExtractedMemory(extracted, context) {
3201
+ const minImportance = providerRegistry.getConfig().minImportance;
3202
+ if (extracted.importance < minImportance)
3203
+ return null;
3204
+ if (!extracted.content.trim())
3205
+ return null;
3206
+ if (isDuplicate(extracted.content, context.agentId, context.projectId)) {
3207
+ return null;
2583
3208
  }
2584
- const raw = readFileSync2(inFile, "utf-8");
2585
- let remoteMemories;
2586
3209
  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++;
3210
+ const input = {
3211
+ key: extracted.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
3212
+ value: extracted.content,
3213
+ category: extracted.category,
3214
+ scope: extracted.suggestedScope,
3215
+ importance: extracted.importance,
3216
+ tags: [
3217
+ ...extracted.tags,
3218
+ "auto-extracted",
3219
+ ...context.sessionId ? [`session:${context.sessionId}`] : []
3220
+ ],
3221
+ agent_id: context.agentId,
3222
+ project_id: context.projectId,
3223
+ session_id: context.sessionId,
3224
+ metadata: {
3225
+ reasoning: extracted.reasoning,
3226
+ auto_extracted: true,
3227
+ extracted_at: new Date().toISOString()
2621
3228
  }
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
- }
3229
+ };
3230
+ const memory = createMemory(input, "merge");
3231
+ return memory.id;
3232
+ } catch (err) {
3233
+ console.error("[auto-memory] saveExtractedMemory failed:", err);
3234
+ return null;
2641
3235
  }
2642
- return { pulled, conflicts };
2643
3236
  }
2644
- function syncMemories(agentName, direction = "both", options = {}) {
2645
- const result = {
2646
- pushed: 0,
2647
- pulled: 0,
2648
- conflicts: 0,
2649
- errors: []
3237
+ async function processJob(job) {
3238
+ if (!providerRegistry.getConfig().enabled)
3239
+ return;
3240
+ const provider = providerRegistry.getAvailable();
3241
+ if (!provider)
3242
+ return;
3243
+ const context = {
3244
+ agentId: job.agentId,
3245
+ projectId: job.projectId,
3246
+ sessionId: job.sessionId
2650
3247
  };
3248
+ let extracted = [];
2651
3249
  try {
2652
- if (direction === "push" || direction === "both") {
2653
- result.pushed = pushMemories(agentName, options.agent_id, options.project_id, options.db);
3250
+ extracted = await provider.extractMemories(job.turn, context);
3251
+ } catch {
3252
+ const fallbacks = providerRegistry.getFallbacks();
3253
+ for (const fallback of fallbacks) {
3254
+ try {
3255
+ extracted = await fallback.extractMemories(job.turn, context);
3256
+ if (extracted.length > 0)
3257
+ break;
3258
+ } catch {
3259
+ continue;
3260
+ }
2654
3261
  }
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;
3262
+ }
3263
+ if (extracted.length === 0)
3264
+ return;
3265
+ for (const memory of extracted) {
3266
+ const memoryId = await saveExtractedMemory(memory, context);
3267
+ if (!memoryId)
3268
+ continue;
3269
+ if (providerRegistry.getConfig().autoEntityLink) {
3270
+ linkEntitiesToMemory(memoryId, memory.content, job.agentId, job.projectId);
2659
3271
  }
2660
- } catch (e) {
2661
- result.errors.push(e instanceof Error ? e.message : String(e));
2662
3272
  }
2663
- return result;
2664
3273
  }
2665
- var defaultSyncAgents = ["claude", "codex", "gemini"];
3274
+ autoMemoryQueue.setHandler(processJob);
3275
+ function processConversationTurn(turn, context, source = "turn") {
3276
+ if (!turn?.trim())
3277
+ return;
3278
+ autoMemoryQueue.enqueue({
3279
+ ...context,
3280
+ turn,
3281
+ timestamp: Date.now(),
3282
+ source
3283
+ });
3284
+ }
3285
+ function getAutoMemoryStats() {
3286
+ return autoMemoryQueue.getStats();
3287
+ }
3288
+ function configureAutoMemory(config) {
3289
+ providerRegistry.configure(config);
3290
+ }
3291
+ // src/lib/dedup.ts
3292
+ var DEFAULT_CONFIG2 = {
3293
+ threshold: 0.8,
3294
+ keepLonger: true
3295
+ };
3296
+ var _stats = { checked: 0, skipped: 0, updated: 0 };
3297
+ function getDedupStats() {
3298
+ return { ..._stats };
3299
+ }
3300
+ function checkDuplicate(content, filter, config = DEFAULT_CONFIG2) {
3301
+ _stats.checked++;
3302
+ const query = content.split(/\s+/).filter((w) => w.length > 3).slice(0, 12).join(" ");
3303
+ if (!query)
3304
+ return "unique";
3305
+ let results;
3306
+ try {
3307
+ results = searchMemories(query, { ...filter, limit: 5 });
3308
+ } catch {
3309
+ return "unique";
3310
+ }
3311
+ if (results.length === 0)
3312
+ return "unique";
3313
+ const contentWords = tokenize(content);
3314
+ if (contentWords.size === 0)
3315
+ return "unique";
3316
+ for (const result of results) {
3317
+ const existingWords = tokenize(result.memory.value);
3318
+ if (existingWords.size === 0)
3319
+ continue;
3320
+ const similarity = jaccardSimilarity(contentWords, existingWords);
3321
+ if (similarity >= config.threshold) {
3322
+ if (config.keepLonger && content.length > result.memory.value.length) {
3323
+ return { updateId: result.memory.id, existingContent: result.memory.value };
3324
+ }
3325
+ return "duplicate";
3326
+ }
3327
+ }
3328
+ return "unique";
3329
+ }
3330
+ function dedup(content, filter, config = DEFAULT_CONFIG2) {
3331
+ const result = checkDuplicate(content, filter, config);
3332
+ if (result === "unique")
3333
+ return "save";
3334
+ if (result === "duplicate") {
3335
+ _stats.skipped++;
3336
+ return "skip";
3337
+ }
3338
+ try {
3339
+ const existing = getMemory(result.updateId);
3340
+ if (!existing)
3341
+ return "save";
3342
+ updateMemory(result.updateId, { value: content, version: existing.version });
3343
+ _stats.updated++;
3344
+ } catch {
3345
+ return "save";
3346
+ }
3347
+ return "skip";
3348
+ }
3349
+ function tokenize(text) {
3350
+ return new Set(text.toLowerCase().split(/\W+/).filter((w) => w.length > 3));
3351
+ }
3352
+ function jaccardSimilarity(a, b) {
3353
+ if (a.size === 0 || b.size === 0)
3354
+ return 0;
3355
+ const intersection = [...a].filter((w) => b.has(w)).length;
3356
+ const union = new Set([...a, ...b]).size;
3357
+ return intersection / union;
3358
+ }
2666
3359
  export {
2667
3360
  withMemoryLock,
2668
3361
  uuid,
@@ -2670,13 +3363,16 @@ export {
2670
3363
  updateEntity,
2671
3364
  updateAgent,
2672
3365
  unlinkEntityFromMemory,
3366
+ unfocus,
2673
3367
  touchMemory,
2674
3368
  touchAgent,
2675
3369
  syncMemories,
2676
3370
  shortUuid,
3371
+ setFocus,
2677
3372
  setActiveProfile,
2678
3373
  searchMemories,
2679
3374
  runCleanup,
3375
+ resolveProjectId,
2680
3376
  resolvePartialId,
2681
3377
  resetDatabase,
2682
3378
  releaseResourceLocks,
@@ -2686,8 +3382,10 @@ export {
2686
3382
  registerProject,
2687
3383
  registerAgent,
2688
3384
  redactSecrets,
3385
+ providerRegistry,
3386
+ processConversationTurn,
2689
3387
  parseRelationRow,
2690
- parseEntityRow,
3388
+ parseEntityRow2 as parseEntityRow,
2691
3389
  now,
2692
3390
  mergeEntities,
2693
3391
  memoryLockId,
@@ -2701,6 +3399,7 @@ export {
2701
3399
  listAgents,
2702
3400
  listAgentLocks,
2703
3401
  linkEntityToMemory,
3402
+ incrementRecallCount,
2704
3403
  getRelation,
2705
3404
  getRelatedEntities,
2706
3405
  getProject,
@@ -2709,17 +3408,20 @@ export {
2709
3408
  getMemory,
2710
3409
  getMemoriesForEntity,
2711
3410
  getMemoriesByKey,
3411
+ getFocus,
2712
3412
  getEntityMemoryLinks,
2713
3413
  getEntityGraph,
2714
3414
  getEntityByName,
2715
3415
  getEntity,
2716
3416
  getEntitiesForMemory,
3417
+ getDedupStats,
2717
3418
  getDbPath,
2718
3419
  getDatabase,
3420
+ getAutoMemoryStats,
2719
3421
  getAgent,
2720
3422
  getActiveProfile,
3423
+ focusFilterSQL,
2721
3424
  findPath,
2722
- extractEntities,
2723
3425
  enforceQuotas,
2724
3426
  deprioritizeStale,
2725
3427
  deleteRelation,
@@ -2727,10 +3429,12 @@ export {
2727
3429
  deleteMemory,
2728
3430
  deleteEntity,
2729
3431
  defaultSyncAgents,
3432
+ dedup,
2730
3433
  createRelation,
2731
3434
  createMemory,
2732
3435
  createEntity,
2733
3436
  containsSecrets,
3437
+ configureAutoMemory,
2734
3438
  closeDatabase,
2735
3439
  cleanExpiredMemories,
2736
3440
  cleanExpiredLocks,
@@ -2738,6 +3442,7 @@ export {
2738
3442
  checkLock,
2739
3443
  bulkLinkEntities,
2740
3444
  bulkDeleteMemories,
3445
+ buildFocusFilter,
2741
3446
  archiveUnused,
2742
3447
  archiveStale,
2743
3448
  agentHoldsLock,