@hasna/mementos 0.4.39 → 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 (44) hide show
  1. package/dist/cli/index.js +2301 -1545
  2. package/dist/db/agents.d.ts +1 -1
  3. package/dist/db/agents.d.ts.map +1 -1
  4. package/dist/db/database.d.ts.map +1 -1
  5. package/dist/db/locks.d.ts +52 -0
  6. package/dist/db/locks.d.ts.map +1 -0
  7. package/dist/db/memories.d.ts +1 -0
  8. package/dist/db/memories.d.ts.map +1 -1
  9. package/dist/index.d.ts +9 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1905 -1003
  12. package/dist/lib/auto-memory-queue.d.ts +46 -0
  13. package/dist/lib/auto-memory-queue.d.ts.map +1 -0
  14. package/dist/lib/auto-memory.d.ts +18 -0
  15. package/dist/lib/auto-memory.d.ts.map +1 -0
  16. package/dist/lib/dedup.d.ts +33 -0
  17. package/dist/lib/dedup.d.ts.map +1 -0
  18. package/dist/lib/focus.d.ts +58 -0
  19. package/dist/lib/focus.d.ts.map +1 -0
  20. package/dist/lib/memory-lock.d.ts +58 -0
  21. package/dist/lib/memory-lock.d.ts.map +1 -0
  22. package/dist/lib/providers/anthropic.d.ts +21 -0
  23. package/dist/lib/providers/anthropic.d.ts.map +1 -0
  24. package/dist/lib/providers/base.d.ts +96 -0
  25. package/dist/lib/providers/base.d.ts.map +1 -0
  26. package/dist/lib/providers/cerebras.d.ts +20 -0
  27. package/dist/lib/providers/cerebras.d.ts.map +1 -0
  28. package/dist/lib/providers/grok.d.ts +19 -0
  29. package/dist/lib/providers/grok.d.ts.map +1 -0
  30. package/dist/lib/providers/index.d.ts +7 -0
  31. package/dist/lib/providers/index.d.ts.map +1 -0
  32. package/dist/lib/providers/openai-compat.d.ts +18 -0
  33. package/dist/lib/providers/openai-compat.d.ts.map +1 -0
  34. package/dist/lib/providers/openai.d.ts +20 -0
  35. package/dist/lib/providers/openai.d.ts.map +1 -0
  36. package/dist/lib/providers/registry.d.ts +38 -0
  37. package/dist/lib/providers/registry.d.ts.map +1 -0
  38. package/dist/lib/search.d.ts.map +1 -1
  39. package/dist/mcp/index.js +1917 -996
  40. package/dist/server/index.d.ts.map +1 -1
  41. package/dist/server/index.js +1602 -894
  42. package/dist/types/index.d.ts +24 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,5 +1,23 @@
1
1
  // @bun
2
2
  // src/types/index.ts
3
+ class AgentConflictError extends Error {
4
+ conflict = true;
5
+ existing_id;
6
+ existing_name;
7
+ last_seen_at;
8
+ session_hint;
9
+ working_dir;
10
+ constructor(opts) {
11
+ const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
12
+ super(msg);
13
+ this.name = "AgentConflictError";
14
+ this.existing_id = opts.existing_id;
15
+ this.existing_name = opts.existing_name;
16
+ this.last_seen_at = opts.last_seen_at;
17
+ this.session_hint = opts.session_hint;
18
+ this.working_dir = opts.working_dir ?? null;
19
+ }
20
+ }
3
21
  class EntityNotFoundError extends Error {
4
22
  constructor(id) {
5
23
  super(`Entity not found: ${id}`);
@@ -301,6 +319,33 @@ var MIGRATIONS = [
301
319
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
302
320
  CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
303
321
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
322
+ `,
323
+ `
324
+ ALTER TABLE agents ADD COLUMN session_id TEXT;
325
+ CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
326
+ INSERT OR IGNORE INTO _migrations (id) VALUES (7);
327
+ `,
328
+ `
329
+ CREATE TABLE IF NOT EXISTS resource_locks (
330
+ id TEXT PRIMARY KEY,
331
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
332
+ resource_id TEXT NOT NULL,
333
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
334
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
335
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
336
+ expires_at TEXT NOT NULL
337
+ );
338
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
339
+ ON resource_locks(resource_type, resource_id)
340
+ WHERE lock_type = 'exclusive';
341
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
342
+ CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
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);
304
349
  `
305
350
  ];
306
351
  var _db = null;
@@ -398,1210 +443,992 @@ function containsSecrets(text) {
398
443
  return false;
399
444
  }
400
445
 
401
- // src/db/agents.ts
402
- function parseAgentRow(row) {
446
+ // src/db/entity-memories.ts
447
+ function parseEntityRow(row) {
403
448
  return {
404
449
  id: row["id"],
405
450
  name: row["name"],
451
+ type: row["type"],
406
452
  description: row["description"] || null,
407
- role: row["role"] || null,
408
453
  metadata: JSON.parse(row["metadata"] || "{}"),
409
- active_project_id: row["active_project_id"] || null,
454
+ project_id: row["project_id"] || null,
410
455
  created_at: row["created_at"],
411
- last_seen_at: row["last_seen_at"]
456
+ updated_at: row["updated_at"]
412
457
  };
413
458
  }
414
- function registerAgent(name, description, role, db) {
415
- const d = db || getDatabase();
416
- const timestamp = now();
417
- const normalizedName = name.trim().toLowerCase();
418
- const existing = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
419
- if (existing) {
420
- const existingId = existing["id"];
421
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [
422
- timestamp,
423
- existingId
424
- ]);
425
- if (description) {
426
- d.run("UPDATE agents SET description = ? WHERE id = ?", [
427
- description,
428
- existingId
429
- ]);
430
- }
431
- if (role) {
432
- d.run("UPDATE agents SET role = ? WHERE id = ?", [
433
- role,
434
- existingId
435
- ]);
436
- }
437
- return getAgent(existingId, d);
438
- }
439
- const id = shortUuid();
440
- d.run("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)", [id, normalizedName, description || null, role || "agent", timestamp, timestamp]);
441
- 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
+ };
442
466
  }
443
- function getAgent(idOrName, db) {
467
+ function linkEntityToMemory(entityId, memoryId, role = "context", db) {
444
468
  const d = db || getDatabase();
445
- let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
446
- if (row)
447
- return parseAgentRow(row);
448
- row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
449
- if (row)
450
- return parseAgentRow(row);
451
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
452
- if (rows.length === 1)
453
- return parseAgentRow(rows[0]);
454
- 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);
455
474
  }
456
- function listAgents(db) {
475
+ function unlinkEntityFromMemory(entityId, memoryId, db) {
457
476
  const d = db || getDatabase();
458
- const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
459
- return rows.map(parseAgentRow);
477
+ d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
460
478
  }
461
- function touchAgent(idOrName, db) {
479
+ function getMemoriesForEntity(entityId, db) {
462
480
  const d = db || getDatabase();
463
- const agent = getAgent(idOrName, d);
464
- if (!agent)
465
- return;
466
- 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);
467
486
  }
468
- function listAgentsByProject(projectId, db) {
487
+ function getEntitiesForMemory(memoryId, db) {
469
488
  const d = db || getDatabase();
470
- const rows = d.query("SELECT * FROM agents WHERE active_project_id = ? ORDER BY last_seen_at DESC").all(projectId);
471
- 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);
472
494
  }
473
- function updateAgent(id, updates, db) {
495
+ function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
474
496
  const d = db || getDatabase();
475
- const agent = getAgent(id, d);
476
- if (!agent)
477
- return null;
478
497
  const timestamp = now();
479
- if (updates.name) {
480
- const normalizedNewName = updates.name.trim().toLowerCase();
481
- if (normalizedNewName !== agent.name) {
482
- const existing = d.query("SELECT id FROM agents WHERE LOWER(name) = ? AND id != ?").get(normalizedNewName, agent.id);
483
- if (existing) {
484
- throw new Error(`Agent name already taken: ${normalizedNewName}`);
485
- }
486
- 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);
487
503
  }
488
- }
489
- if (updates.description !== undefined) {
490
- d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
491
- }
492
- if (updates.role !== undefined) {
493
- d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
494
- }
495
- if (updates.metadata !== undefined) {
496
- d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
497
- }
498
- if ("active_project_id" in updates) {
499
- d.run("UPDATE agents SET active_project_id = ? WHERE id = ?", [updates.active_project_id ?? null, agent.id]);
500
- }
501
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
502
- return getAgent(agent.id, d);
503
- }
504
-
505
- // src/db/projects.ts
506
- function parseProjectRow(row) {
507
- return {
508
- id: row["id"],
509
- name: row["name"],
510
- path: row["path"],
511
- description: row["description"] || null,
512
- memory_prefix: row["memory_prefix"] || null,
513
- created_at: row["created_at"],
514
- updated_at: row["updated_at"]
515
- };
516
- }
517
- function registerProject(name, path, description, memoryPrefix, db) {
518
- const d = db || getDatabase();
519
- const timestamp = now();
520
- const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
521
- if (existing) {
522
- const existingId = existing["id"];
523
- d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
524
- timestamp,
525
- existingId
526
- ]);
527
- return parseProjectRow(existing);
528
- }
529
- const id = uuid();
530
- 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]);
531
- return getProject(id, d);
532
- }
533
- function getProject(idOrPath, db) {
534
- const d = db || getDatabase();
535
- let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
536
- if (row)
537
- return parseProjectRow(row);
538
- row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
539
- if (row)
540
- return parseProjectRow(row);
541
- row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
542
- if (row)
543
- return parseProjectRow(row);
544
- return null;
504
+ });
505
+ tx();
545
506
  }
546
- function listProjects(db) {
507
+ function getEntityMemoryLinks(entityId, memoryId, db) {
547
508
  const d = db || getDatabase();
548
- const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
549
- return rows.map(parseProjectRow);
550
- }
551
-
552
- // src/lib/extractor.ts
553
- var TECH_KEYWORDS = new Set([
554
- "typescript",
555
- "javascript",
556
- "python",
557
- "rust",
558
- "go",
559
- "java",
560
- "ruby",
561
- "swift",
562
- "kotlin",
563
- "react",
564
- "vue",
565
- "angular",
566
- "svelte",
567
- "nextjs",
568
- "bun",
569
- "node",
570
- "deno",
571
- "sqlite",
572
- "postgres",
573
- "mysql",
574
- "redis",
575
- "docker",
576
- "kubernetes",
577
- "git",
578
- "npm",
579
- "yarn",
580
- "pnpm",
581
- "webpack",
582
- "vite",
583
- "tailwind",
584
- "prisma",
585
- "drizzle",
586
- "zod",
587
- "commander",
588
- "express",
589
- "fastify",
590
- "hono"
591
- ]);
592
- var FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)?(?:[\w.-]+\/)+[\w.-]+\.\w+)/g;
593
- var URL_RE = /https?:\/\/[^\s)]+/g;
594
- var NPM_PACKAGE_RE = /@[\w-]+\/[\w.-]+/g;
595
- var PASCAL_CASE_RE = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
596
- function getSearchText(memory) {
597
- const parts = [memory.key, memory.value];
598
- if (memory.summary)
599
- parts.push(memory.summary);
600
- return parts.join(" ");
601
- }
602
- function extractEntities(memory, db) {
603
- const text = getSearchText(memory);
604
- const entityMap = new Map;
605
- function add(name, type, confidence) {
606
- const normalized = name.toLowerCase();
607
- if (normalized.length < 3)
608
- return;
609
- const existing = entityMap.get(normalized);
610
- if (!existing || existing.confidence < confidence) {
611
- entityMap.set(normalized, { name: normalized, type, confidence });
612
- }
613
- }
614
- for (const match of text.matchAll(FILE_PATH_RE)) {
615
- add(match[1].trim(), "file", 0.9);
616
- }
617
- for (const match of text.matchAll(URL_RE)) {
618
- add(match[0], "api", 0.8);
619
- }
620
- for (const match of text.matchAll(NPM_PACKAGE_RE)) {
621
- add(match[0], "tool", 0.85);
509
+ const conditions = [];
510
+ const params = [];
511
+ if (entityId) {
512
+ conditions.push("entity_id = ?");
513
+ params.push(entityId);
622
514
  }
623
- try {
624
- const d = db || getDatabase();
625
- const agents = listAgents(d);
626
- const textLower2 = text.toLowerCase();
627
- for (const agent of agents) {
628
- const nameLower = agent.name.toLowerCase();
629
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
630
- add(agent.name, "person", 0.95);
631
- }
632
- }
633
- } catch {}
634
- try {
635
- const d = db || getDatabase();
636
- const projects = listProjects(d);
637
- const textLower2 = text.toLowerCase();
638
- for (const project of projects) {
639
- const nameLower = project.name.toLowerCase();
640
- if (nameLower.length >= 3 && textLower2.includes(nameLower)) {
641
- add(project.name, "project", 0.95);
642
- }
643
- }
644
- } catch {}
645
- const textLower = text.toLowerCase();
646
- for (const keyword of TECH_KEYWORDS) {
647
- const re = new RegExp(`\\b${keyword}\\b`, "i");
648
- if (re.test(textLower)) {
649
- add(keyword, "tool", 0.7);
650
- }
515
+ if (memoryId) {
516
+ conditions.push("memory_id = ?");
517
+ params.push(memoryId);
651
518
  }
652
- for (const match of text.matchAll(PASCAL_CASE_RE)) {
653
- add(match[1], "concept", 0.5);
519
+ let sql = "SELECT * FROM entity_memories";
520
+ if (conditions.length > 0) {
521
+ sql += ` WHERE ${conditions.join(" AND ")}`;
654
522
  }
655
- 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);
656
526
  }
657
527
 
658
- // src/db/entities.ts
659
- function parseEntityRow(row) {
528
+ // src/db/memories.ts
529
+ function runEntityExtraction(_memory, _projectId, _d) {}
530
+ function parseMemoryRow(row) {
660
531
  return {
661
532
  id: row["id"],
662
- name: row["name"],
663
- type: row["type"],
664
- description: row["description"] || null,
665
- 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,
666
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,
667
550
  created_at: row["created_at"],
668
- updated_at: row["updated_at"]
551
+ updated_at: row["updated_at"],
552
+ accessed_at: row["accessed_at"] || null
669
553
  };
670
554
  }
671
- function createEntity(input, db) {
555
+ function createMemory(input, dedupeMode = "merge", db) {
672
556
  const d = db || getDatabase();
673
557
  const timestamp = now();
674
- const metadataJson = JSON.stringify(input.metadata || {});
675
- const existing = d.query(`SELECT * FROM entities
676
- WHERE name = ? AND type = ? AND COALESCE(project_id, '') = ?`).get(input.name, input.type, input.project_id || "");
677
- if (existing) {
678
- const sets = ["updated_at = ?"];
679
- const params = [timestamp];
680
- if (input.description !== undefined) {
681
- sets.push("description = ?");
682
- params.push(input.description);
683
- }
684
- if (input.metadata !== undefined) {
685
- sets.push("metadata = ?");
686
- params.push(metadataJson);
687
- }
688
- const existingId = existing["id"];
689
- params.push(existingId);
690
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
691
- return getEntity(existingId, d);
558
+ let expiresAt = input.expires_at || null;
559
+ if (input.ttl_ms && !expiresAt) {
560
+ expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
692
561
  }
693
- const id = shortUuid();
694
- d.run(`INSERT INTO entities (id, name, type, description, metadata, project_id, created_at, updated_at)
695
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
562
+ const id = uuid();
563
+ const tags = input.tags || [];
564
+ const tagsJson = JSON.stringify(tags);
565
+ const metadataJson = JSON.stringify(input.metadata || {});
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;
605
+ }
606
+ }
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, ?, ?, ?)`, [
696
609
  id,
697
- input.name,
698
- input.type,
699
- input.description || null,
700
- 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,
701
619
  input.project_id || null,
620
+ input.session_id || null,
621
+ metadataJson,
622
+ expiresAt,
702
623
  timestamp,
703
624
  timestamp
704
625
  ]);
705
- 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;
706
635
  }
707
- function getEntity(id, db) {
636
+ function getMemory(id, db) {
708
637
  const d = db || getDatabase();
709
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
638
+ const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
710
639
  if (!row)
711
- throw new EntityNotFoundError(id);
712
- return parseEntityRow(row);
640
+ return null;
641
+ return parseMemoryRow(row);
713
642
  }
714
- function getEntityByName(name, type, projectId, db) {
643
+ function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
715
644
  const d = db || getDatabase();
716
- let sql = "SELECT * FROM entities WHERE name = ?";
717
- const params = [name];
718
- if (type) {
719
- sql += " AND type = ?";
720
- 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);
721
650
  }
722
- if (projectId !== undefined) {
651
+ if (agentId) {
652
+ sql += " AND agent_id = ?";
653
+ params.push(agentId);
654
+ }
655
+ if (projectId) {
723
656
  sql += " AND project_id = ?";
724
657
  params.push(projectId);
725
658
  }
726
- 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";
727
664
  const row = d.query(sql).get(...params);
728
665
  if (!row)
729
666
  return null;
730
- return parseEntityRow(row);
667
+ return parseMemoryRow(row);
731
668
  }
732
- function listEntities(filter = {}, db) {
669
+ function getMemoriesByKey(key, scope, agentId, projectId, db) {
733
670
  const d = db || getDatabase();
734
- const conditions = [];
735
- const params = [];
736
- if (filter.type) {
737
- conditions.push("type = ?");
738
- 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);
739
676
  }
740
- if (filter.project_id) {
741
- conditions.push("project_id = ?");
742
- params.push(filter.project_id);
677
+ if (agentId) {
678
+ sql += " AND agent_id = ?";
679
+ params.push(agentId);
743
680
  }
744
- if (filter.search) {
745
- conditions.push("(name LIKE ? OR description LIKE ?)");
746
- const term = `%${filter.search}%`;
747
- params.push(term, term);
681
+ if (projectId) {
682
+ sql += " AND project_id = ?";
683
+ params.push(projectId);
748
684
  }
749
- 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";
750
767
  if (conditions.length > 0) {
751
768
  sql += ` WHERE ${conditions.join(" AND ")}`;
752
769
  }
753
- sql += " ORDER BY updated_at DESC";
754
- if (filter.limit) {
770
+ sql += " ORDER BY importance DESC, created_at DESC";
771
+ if (filter?.limit) {
755
772
  sql += " LIMIT ?";
756
773
  params.push(filter.limit);
757
774
  }
758
- if (filter.offset) {
775
+ if (filter?.offset) {
759
776
  sql += " OFFSET ?";
760
777
  params.push(filter.offset);
761
778
  }
762
779
  const rows = d.query(sql).all(...params);
763
- return rows.map(parseEntityRow);
780
+ return rows.map(parseMemoryRow);
764
781
  }
765
- function updateEntity(id, input, db) {
782
+ function updateMemory(id, input, db) {
766
783
  const d = db || getDatabase();
767
- const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
784
+ const existing = getMemory(id, d);
768
785
  if (!existing)
769
- throw new EntityNotFoundError(id);
770
- const sets = ["updated_at = ?"];
786
+ throw new MemoryNotFoundError(id);
787
+ if (existing.version !== input.version) {
788
+ throw new VersionConflictError(id, input.version, existing.version);
789
+ }
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 = ?"];
771
808
  const params = [now()];
772
- if (input.name !== undefined) {
773
- sets.push("name = ?");
774
- params.push(input.name);
809
+ if (input.value !== undefined) {
810
+ sets.push("value = ?");
811
+ params.push(redactSecrets(input.value));
775
812
  }
776
- if (input.type !== undefined) {
777
- sets.push("type = ?");
778
- params.push(input.type);
813
+ if (input.category !== undefined) {
814
+ sets.push("category = ?");
815
+ params.push(input.category);
779
816
  }
780
- if (input.description !== undefined) {
781
- sets.push("description = ?");
782
- params.push(input.description);
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);
783
836
  }
784
837
  if (input.metadata !== undefined) {
785
838
  sets.push("metadata = ?");
786
839
  params.push(JSON.stringify(input.metadata));
787
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
+ }
788
854
  params.push(id);
789
- d.run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, params);
790
- return getEntity(id, d);
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;
791
867
  }
792
- function deleteEntity(id, db) {
868
+ function deleteMemory(id, db) {
793
869
  const d = db || getDatabase();
794
- const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
795
- if (result.changes === 0)
796
- throw new EntityNotFoundError(id);
870
+ const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
871
+ return result.changes > 0;
797
872
  }
798
- function mergeEntities(sourceId, targetId, db) {
873
+ function bulkDeleteMemories(ids, db) {
799
874
  const d = db || getDatabase();
800
- getEntity(sourceId, d);
801
- getEntity(targetId, d);
802
- d.run(`UPDATE OR IGNORE relations SET source_entity_id = ? WHERE source_entity_id = ?`, [targetId, sourceId]);
803
- d.run(`UPDATE OR IGNORE relations SET target_entity_id = ? WHERE target_entity_id = ?`, [targetId, sourceId]);
804
- d.run("DELETE FROM relations WHERE source_entity_id = ? OR target_entity_id = ?", [
805
- sourceId,
806
- sourceId
807
- ]);
808
- d.run(`UPDATE OR IGNORE entity_memories SET entity_id = ? WHERE entity_id = ?`, [targetId, sourceId]);
809
- d.run("DELETE FROM entity_memories WHERE entity_id = ?", [sourceId]);
810
- d.run("DELETE FROM entities WHERE id = ?", [sourceId]);
811
- d.run("UPDATE entities SET updated_at = ? WHERE id = ?", [now(), targetId]);
812
- return getEntity(targetId, d);
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;
813
884
  }
814
-
815
- // src/db/entity-memories.ts
816
- function parseEntityRow2(row) {
885
+ function touchMemory(id, db) {
886
+ const d = db || getDatabase();
887
+ d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
888
+ }
889
+ var RECALL_PROMOTE_THRESHOLD = 3;
890
+ function incrementRecallCount(id, db) {
891
+ const d = db || getDatabase();
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 {}
903
+ }
904
+ function cleanExpiredMemories(db) {
905
+ const d = db || getDatabase();
906
+ const timestamp = now();
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;
913
+ }
914
+ function getMemoryVersions(memoryId, db) {
915
+ const d = db || getDatabase();
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 [];
934
+ }
935
+ }
936
+ // src/db/agents.ts
937
+ var CONFLICT_WINDOW_MS = 30 * 60 * 1000;
938
+ function parseAgentRow(row) {
817
939
  return {
818
940
  id: row["id"],
819
941
  name: row["name"],
820
- type: row["type"],
942
+ session_id: row["session_id"] || null,
821
943
  description: row["description"] || null,
944
+ role: row["role"] || null,
822
945
  metadata: JSON.parse(row["metadata"] || "{}"),
823
- project_id: row["project_id"] || null,
946
+ active_project_id: row["active_project_id"] || null,
824
947
  created_at: row["created_at"],
825
- updated_at: row["updated_at"]
826
- };
827
- }
828
- function parseEntityMemoryRow(row) {
829
- return {
830
- entity_id: row["entity_id"],
831
- memory_id: row["memory_id"],
832
- role: row["role"],
833
- created_at: row["created_at"]
948
+ last_seen_at: row["last_seen_at"]
834
949
  };
835
950
  }
836
- function linkEntityToMemory(entityId, memoryId, role = "context", db) {
951
+ function registerAgent(name, sessionId, description, role, projectId, db) {
837
952
  const d = db || getDatabase();
838
953
  const timestamp = now();
839
- d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
840
- VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
841
- const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
842
- return parseEntityMemoryRow(row);
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
+ }
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);
988
+ }
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);
843
992
  }
844
- function unlinkEntityFromMemory(entityId, memoryId, db) {
993
+ function getAgent(idOrName, db) {
845
994
  const d = db || getDatabase();
846
- d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
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;
847
1005
  }
848
- function getMemoriesForEntity(entityId, db) {
1006
+ function listAgents(db) {
849
1007
  const d = db || getDatabase();
850
- const rows = d.query(`SELECT m.* FROM memories m
851
- INNER JOIN entity_memories em ON em.memory_id = m.id
852
- WHERE em.entity_id = ?
853
- ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
854
- return rows.map(parseMemoryRow);
1008
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
1009
+ return rows.map(parseAgentRow);
855
1010
  }
856
- function getEntitiesForMemory(memoryId, db) {
1011
+ function touchAgent(idOrName, db) {
857
1012
  const d = db || getDatabase();
858
- const rows = d.query(`SELECT e.* FROM entities e
859
- INNER JOIN entity_memories em ON em.entity_id = e.id
860
- WHERE em.memory_id = ?
861
- ORDER BY e.name ASC`).all(memoryId);
862
- return rows.map(parseEntityRow2);
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]);
863
1017
  }
864
- function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
1018
+ function listAgentsByProject(projectId, db) {
865
1019
  const d = db || getDatabase();
866
- const timestamp = now();
867
- const tx = d.transaction(() => {
868
- const stmt = d.prepare(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
869
- VALUES (?, ?, ?, ?)`);
870
- for (const entityId of entityIds) {
871
- stmt.run(entityId, memoryId, role, timestamp);
872
- }
873
- });
874
- tx();
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);
875
1022
  }
876
- function getEntityMemoryLinks(entityId, memoryId, db) {
1023
+ function updateAgent(id, updates, db) {
877
1024
  const d = db || getDatabase();
878
- const conditions = [];
879
- const params = [];
880
- if (entityId) {
881
- conditions.push("entity_id = ?");
882
- params.push(entityId);
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]);
1037
+ }
883
1038
  }
884
- if (memoryId) {
885
- conditions.push("memory_id = ?");
886
- params.push(memoryId);
1039
+ if (updates.description !== undefined) {
1040
+ d.run("UPDATE agents SET description = ? WHERE id = ?", [updates.description, agent.id]);
887
1041
  }
888
- let sql = "SELECT * FROM entity_memories";
889
- if (conditions.length > 0) {
890
- sql += ` WHERE ${conditions.join(" AND ")}`;
1042
+ if (updates.role !== undefined) {
1043
+ d.run("UPDATE agents SET role = ? WHERE id = ?", [updates.role, agent.id]);
891
1044
  }
892
- sql += " ORDER BY created_at DESC";
893
- const rows = d.query(sql).all(...params);
894
- return rows.map(parseEntityMemoryRow);
895
- }
896
-
897
- // src/db/relations.ts
898
- function parseRelationRow(row) {
899
- return {
900
- id: row["id"],
901
- source_entity_id: row["source_entity_id"],
902
- target_entity_id: row["target_entity_id"],
903
- relation_type: row["relation_type"],
904
- weight: row["weight"],
905
- metadata: JSON.parse(row["metadata"] || "{}"),
906
- created_at: row["created_at"]
907
- };
1045
+ if (updates.metadata !== undefined) {
1046
+ d.run("UPDATE agents SET metadata = ? WHERE id = ?", [JSON.stringify(updates.metadata), agent.id]);
1047
+ }
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]);
1050
+ }
1051
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [timestamp, agent.id]);
1052
+ return getAgent(agent.id, d);
908
1053
  }
909
- function parseEntityRow3(row) {
1054
+ // src/db/locks.ts
1055
+ function parseLockRow(row) {
910
1056
  return {
911
1057
  id: row["id"],
912
- name: row["name"],
913
- type: row["type"],
914
- description: row["description"] || null,
915
- metadata: JSON.parse(row["metadata"] || "{}"),
916
- project_id: row["project_id"] || null,
917
- created_at: row["created_at"],
918
- updated_at: row["updated_at"]
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"]
919
1064
  };
920
1065
  }
921
- function createRelation(input, db) {
1066
+ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
922
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 });
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
+ }
923
1084
  const id = shortUuid();
924
- const timestamp = now();
925
- const weight = input.weight ?? 1;
926
- const metadata = JSON.stringify(input.metadata ?? {});
927
- d.run(`INSERT INTO relations (id, source_entity_id, target_entity_id, relation_type, weight, metadata, created_at)
928
- VALUES (?, ?, ?, ?, ?, ?, ?)
929
- ON CONFLICT(source_entity_id, target_entity_id, relation_type)
930
- DO UPDATE SET weight = excluded.weight, metadata = excluded.metadata`, [id, input.source_entity_id, input.target_entity_id, input.relation_type, weight, metadata, timestamp]);
931
- const row = d.query(`SELECT * FROM relations
932
- WHERE source_entity_id = ? AND target_entity_id = ? AND relation_type = ?`).get(input.source_entity_id, input.target_entity_id, input.relation_type);
933
- return parseRelationRow(row);
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
+ };
934
1097
  }
935
- function getRelation(id, db) {
1098
+ function releaseLock(lockId, agentId, db) {
936
1099
  const d = db || getDatabase();
937
- const row = d.query("SELECT * FROM relations WHERE id = ?").get(id);
938
- if (!row)
939
- throw new Error(`Relation not found: ${id}`);
940
- return parseRelationRow(row);
1100
+ const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
1101
+ return result.changes > 0;
941
1102
  }
942
- function listRelations(filter, db) {
1103
+ function releaseResourceLocks(agentId, resourceType, resourceId, db) {
943
1104
  const d = db || getDatabase();
944
- const conditions = [];
945
- const params = [];
946
- if (filter.entity_id) {
947
- const dir = filter.direction || "both";
948
- if (dir === "outgoing") {
949
- conditions.push("source_entity_id = ?");
950
- params.push(filter.entity_id);
951
- } else if (dir === "incoming") {
952
- conditions.push("target_entity_id = ?");
953
- params.push(filter.entity_id);
954
- } else {
955
- conditions.push("(source_entity_id = ? OR target_entity_id = ?)");
956
- params.push(filter.entity_id, filter.entity_id);
957
- }
958
- }
959
- if (filter.relation_type) {
960
- conditions.push("relation_type = ?");
961
- params.push(filter.relation_type);
962
- }
963
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
964
- const rows = d.query(`SELECT * FROM relations ${where} ORDER BY created_at DESC`).all(...params);
965
- return rows.map(parseRelationRow);
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;
966
1107
  }
967
- function deleteRelation(id, db) {
1108
+ function releaseAllAgentLocks(agentId, db) {
968
1109
  const d = db || getDatabase();
969
- const result = d.run("DELETE FROM relations WHERE id = ?", [id]);
970
- if (result.changes === 0)
971
- throw new Error(`Relation not found: ${id}`);
1110
+ const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
1111
+ return result.changes;
972
1112
  }
973
- function getRelatedEntities(entityId, relationType, db) {
1113
+ function checkLock(resourceType, resourceId, lockType, db) {
974
1114
  const d = db || getDatabase();
975
- let sql;
976
- const params = [];
977
- if (relationType) {
978
- sql = `
979
- SELECT DISTINCT e.* FROM entities e
980
- JOIN relations r ON (
981
- (r.source_entity_id = ? AND r.target_entity_id = e.id)
982
- OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
983
- )
984
- WHERE r.relation_type = ?
985
- `;
986
- params.push(entityId, entityId, relationType);
987
- } else {
988
- sql = `
989
- SELECT DISTINCT e.* FROM entities e
990
- JOIN relations r ON (
991
- (r.source_entity_id = ? AND r.target_entity_id = e.id)
992
- OR (r.target_entity_id = ? AND r.source_entity_id = e.id)
993
- )
994
- `;
995
- params.push(entityId, entityId);
996
- }
997
- const rows = d.query(sql).all(...params);
998
- return rows.map(parseEntityRow3);
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);
999
1119
  }
1000
- function getEntityGraph(entityId, depth = 2, db) {
1120
+ function agentHoldsLock(agentId, resourceType, resourceId, lockType, db) {
1001
1121
  const d = db || getDatabase();
1002
- const entityRows = d.query(`WITH RECURSIVE graph(id, depth) AS (
1003
- VALUES(?, 0)
1004
- UNION
1005
- SELECT CASE WHEN r.source_entity_id = g.id THEN r.target_entity_id ELSE r.source_entity_id END, g.depth + 1
1006
- FROM relations r JOIN graph g ON (r.source_entity_id = g.id OR r.target_entity_id = g.id)
1007
- WHERE g.depth < ?
1008
- )
1009
- SELECT DISTINCT e.* FROM entities e JOIN graph g ON e.id = g.id`).all(entityId, depth);
1010
- const entities = entityRows.map(parseEntityRow3);
1011
- const entityIds = new Set(entities.map((e) => e.id));
1012
- if (entityIds.size === 0) {
1013
- return { entities: [], relations: [] };
1014
- }
1015
- const placeholders = Array.from(entityIds).map(() => "?").join(",");
1016
- const relationRows = d.query(`SELECT * FROM relations
1017
- WHERE source_entity_id IN (${placeholders})
1018
- AND target_entity_id IN (${placeholders})`).all(...Array.from(entityIds), ...Array.from(entityIds));
1019
- const relations = relationRows.map(parseRelationRow);
1020
- return { entities, relations };
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;
1021
1125
  }
1022
- function findPath(fromEntityId, toEntityId, maxDepth = 5, db) {
1126
+ function listAgentLocks(agentId, db) {
1023
1127
  const d = db || getDatabase();
1024
- const rows = d.query(`WITH RECURSIVE path(id, trail, depth) AS (
1025
- SELECT ?, ?, 0
1026
- UNION
1027
- SELECT
1028
- CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1029
- p.trail || ',' || CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END,
1030
- p.depth + 1
1031
- FROM relations r JOIN path p ON (r.source_entity_id = p.id OR r.target_entity_id = p.id)
1032
- WHERE p.depth < ?
1033
- AND INSTR(p.trail, CASE WHEN r.source_entity_id = p.id THEN r.target_entity_id ELSE r.source_entity_id END) = 0
1034
- )
1035
- SELECT trail FROM path WHERE id = ? ORDER BY depth ASC LIMIT 1`).get(fromEntityId, fromEntityId, maxDepth, toEntityId);
1036
- if (!rows)
1037
- return null;
1038
- const ids = rows.trail.split(",");
1039
- const entities = [];
1040
- for (const id of ids) {
1041
- const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1042
- if (row)
1043
- entities.push(parseEntityRow3(row));
1044
- }
1045
- return entities.length > 0 ? entities : null;
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);
1046
1131
  }
1047
-
1048
- // src/lib/config.ts
1049
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
1050
- import { homedir } from "os";
1051
- import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
1052
- var DEFAULT_CONFIG = {
1053
- default_scope: "private",
1054
- default_category: "knowledge",
1055
- default_importance: 5,
1056
- max_entries: 1000,
1057
- max_entries_per_scope: {
1058
- global: 500,
1059
- shared: 300,
1060
- private: 200
1061
- },
1062
- injection: {
1063
- max_tokens: 500,
1064
- min_importance: 5,
1065
- categories: ["preference", "fact"],
1066
- refresh_interval: 5
1067
- },
1068
- extraction: {
1069
- enabled: true,
1070
- min_confidence: 0.5
1071
- },
1072
- sync_agents: ["claude", "codex", "gemini"],
1073
- auto_cleanup: {
1074
- enabled: true,
1075
- expired_check_interval: 3600,
1076
- unused_archive_days: 7,
1077
- stale_deprioritize_days: 14
1078
- }
1079
- };
1080
- function deepMerge(target, source) {
1081
- const result = { ...target };
1082
- for (const key of Object.keys(source)) {
1083
- const sourceVal = source[key];
1084
- const targetVal = result[key];
1085
- if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
1086
- result[key] = deepMerge(targetVal, sourceVal);
1087
- } else {
1088
- result[key] = sourceVal;
1089
- }
1090
- }
1091
- return result;
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;
1092
1136
  }
1093
- var VALID_SCOPES = ["global", "shared", "private"];
1094
- var VALID_CATEGORIES = [
1095
- "preference",
1096
- "fact",
1097
- "knowledge",
1098
- "history"
1099
- ];
1100
- function isValidScope(value) {
1101
- return VALID_SCOPES.includes(value);
1137
+ // src/lib/memory-lock.ts
1138
+ var MEMORY_WRITE_TTL = 30;
1139
+ function memoryLockId(key, scope, projectId) {
1140
+ return `${scope}:${key}:${projectId ?? ""}`;
1102
1141
  }
1103
- function isValidCategory(value) {
1104
- return VALID_CATEGORIES.includes(value);
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);
1105
1145
  }
1106
- function loadConfig() {
1107
- const configPath = join2(homedir(), ".mementos", "config.json");
1108
- let fileConfig = {};
1109
- if (existsSync2(configPath)) {
1110
- try {
1111
- const raw = readFileSync(configPath, "utf-8");
1112
- fileConfig = JSON.parse(raw);
1113
- } catch {}
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");
1114
1161
  }
1115
- const merged = deepMerge(DEFAULT_CONFIG, fileConfig);
1116
- const envScope = process.env["MEMENTOS_DEFAULT_SCOPE"];
1117
- if (envScope && isValidScope(envScope)) {
1118
- merged.default_scope = envScope;
1162
+ try {
1163
+ return fn();
1164
+ } finally {
1165
+ releaseLock(lock.id, agentId, d);
1119
1166
  }
1120
- const envCategory = process.env["MEMENTOS_DEFAULT_CATEGORY"];
1121
- if (envCategory && isValidCategory(envCategory)) {
1122
- merged.default_category = envCategory;
1167
+ }
1168
+
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;
1180
+ }
1181
+ }
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;
1123
1203
  }
1124
- const envImportance = process.env["MEMENTOS_DEFAULT_IMPORTANCE"];
1125
- if (envImportance) {
1126
- const parsed = parseInt(envImportance, 10);
1127
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
1128
- merged.default_importance = parsed;
1129
- }
1204
+ if (agentId) {
1205
+ return getFocus(agentId);
1130
1206
  }
1131
- return merged;
1132
- }
1133
- function profilesDir() {
1134
- return join2(homedir(), ".mementos", "profiles");
1207
+ return null;
1135
1208
  }
1136
- function globalConfigPath() {
1137
- return join2(homedir(), ".mementos", "config.json");
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
+ };
1138
1222
  }
1139
- function readGlobalConfig() {
1140
- const p = globalConfigPath();
1141
- if (!existsSync2(p))
1142
- return {};
1143
- try {
1144
- return JSON.parse(readFileSync(p, "utf-8"));
1145
- } catch {
1146
- return {};
1147
- }
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
+ };
1148
1228
  }
1149
- function writeGlobalConfig(data) {
1150
- const p = globalConfigPath();
1151
- ensureDir2(dirname2(p));
1152
- writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
1153
- }
1154
- function getActiveProfile() {
1155
- const envProfile = process.env["MEMENTOS_PROFILE"];
1156
- if (envProfile)
1157
- return envProfile.trim();
1158
- const cfg = readGlobalConfig();
1159
- return cfg["active_profile"] || null;
1229
+ // src/db/projects.ts
1230
+ function parseProjectRow(row) {
1231
+ return {
1232
+ id: row["id"],
1233
+ name: row["name"],
1234
+ path: row["path"],
1235
+ description: row["description"] || null,
1236
+ memory_prefix: row["memory_prefix"] || null,
1237
+ created_at: row["created_at"],
1238
+ updated_at: row["updated_at"]
1239
+ };
1160
1240
  }
1161
- function setActiveProfile(name) {
1162
- const cfg = readGlobalConfig();
1163
- if (name === null) {
1164
- delete cfg["active_profile"];
1165
- } else {
1166
- cfg["active_profile"] = name;
1241
+ function registerProject(name, path, description, memoryPrefix, db) {
1242
+ const d = db || getDatabase();
1243
+ const timestamp = now();
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);
1167
1252
  }
1168
- writeGlobalConfig(cfg);
1169
- }
1170
- function listProfiles() {
1171
- const dir = profilesDir();
1172
- if (!existsSync2(dir))
1173
- return [];
1174
- return readdirSync(dir).filter((f) => f.endsWith(".db")).map((f) => basename(f, ".db")).sort();
1175
- }
1176
- function deleteProfile(name) {
1177
- const dbPath = join2(profilesDir(), `${name}.db`);
1178
- if (!existsSync2(dbPath))
1179
- return false;
1180
- unlinkSync(dbPath);
1181
- if (getActiveProfile() === name)
1182
- setActiveProfile(null);
1183
- return true;
1253
+ const id = uuid();
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);
1184
1256
  }
1185
- function ensureDir2(dir) {
1186
- if (!existsSync2(dir)) {
1187
- mkdirSync2(dir, { recursive: true });
1188
- }
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;
1189
1269
  }
1190
-
1191
- // src/db/memories.ts
1192
- function runEntityExtraction(memory, projectId, d) {
1193
- const config = loadConfig();
1194
- if (config.extraction?.enabled === false)
1195
- return;
1196
- const extracted = extractEntities(memory, d);
1197
- const minConfidence = config.extraction?.min_confidence ?? 0.5;
1198
- const entityIds = [];
1199
- for (const ext of extracted) {
1200
- if (ext.confidence >= minConfidence) {
1201
- const entity = createEntity({ name: ext.name, type: ext.type, project_id: projectId }, d);
1202
- linkEntityToMemory(entity.id, memory.id, "context", d);
1203
- entityIds.push(entity.id);
1204
- }
1205
- }
1206
- for (let i = 0;i < entityIds.length; i++) {
1207
- for (let j = i + 1;j < entityIds.length; j++) {
1208
- try {
1209
- createRelation({ source_entity_id: entityIds[i], target_entity_id: entityIds[j], relation_type: "related_to" }, d);
1210
- } catch {}
1211
- }
1212
- }
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);
1213
1274
  }
1214
- function parseMemoryRow(row) {
1275
+ // src/db/entities.ts
1276
+ function parseEntityRow2(row) {
1215
1277
  return {
1216
1278
  id: row["id"],
1217
- key: row["key"],
1218
- value: row["value"],
1219
- category: row["category"],
1220
- scope: row["scope"],
1221
- summary: row["summary"] || null,
1222
- tags: JSON.parse(row["tags"] || "[]"),
1223
- importance: row["importance"],
1224
- source: row["source"],
1225
- status: row["status"],
1226
- pinned: !!row["pinned"],
1227
- agent_id: row["agent_id"] || null,
1228
- project_id: row["project_id"] || null,
1229
- session_id: row["session_id"] || null,
1279
+ name: row["name"],
1280
+ type: row["type"],
1281
+ description: row["description"] || null,
1230
1282
  metadata: JSON.parse(row["metadata"] || "{}"),
1231
- access_count: row["access_count"],
1232
- version: row["version"],
1233
- expires_at: row["expires_at"] || null,
1283
+ project_id: row["project_id"] || null,
1234
1284
  created_at: row["created_at"],
1235
- updated_at: row["updated_at"],
1236
- accessed_at: row["accessed_at"] || null
1285
+ updated_at: row["updated_at"]
1237
1286
  };
1238
1287
  }
1239
- function createMemory(input, dedupeMode = "merge", db) {
1288
+ function createEntity(input, db) {
1240
1289
  const d = db || getDatabase();
1241
1290
  const timestamp = now();
1242
- let expiresAt = input.expires_at || null;
1243
- if (input.ttl_ms && !expiresAt) {
1244
- expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
1245
- }
1246
- const id = uuid();
1247
- const tags = input.tags || [];
1248
- const tagsJson = JSON.stringify(tags);
1249
1291
  const metadataJson = JSON.stringify(input.metadata || {});
1250
- const safeValue = redactSecrets(input.value);
1251
- const safeSummary = input.summary ? redactSecrets(input.summary) : null;
1252
- if (dedupeMode === "merge") {
1253
- const existing = d.query(`SELECT id, version FROM memories
1254
- WHERE key = ? AND scope = ?
1255
- AND COALESCE(agent_id, '') = ?
1256
- AND COALESCE(project_id, '') = ?
1257
- AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
1258
- if (existing) {
1259
- d.run(`UPDATE memories SET
1260
- value = ?, category = ?, summary = ?, tags = ?,
1261
- importance = ?, metadata = ?, expires_at = ?,
1262
- pinned = COALESCE(pinned, 0),
1263
- version = version + 1, updated_at = ?
1264
- WHERE id = ?`, [
1265
- safeValue,
1266
- input.category || "knowledge",
1267
- safeSummary,
1268
- tagsJson,
1269
- input.importance ?? 5,
1270
- metadataJson,
1271
- expiresAt,
1272
- timestamp,
1273
- existing.id
1274
- ]);
1275
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
1276
- const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1277
- for (const tag of tags) {
1278
- insertTag2.run(existing.id, tag);
1279
- }
1280
- const merged = getMemory(existing.id, d);
1281
- try {
1282
- const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
1283
- for (const link of oldLinks) {
1284
- unlinkEntityFromMemory(link.entity_id, merged.id, d);
1285
- }
1286
- runEntityExtraction(merged, input.project_id, d);
1287
- } catch {}
1288
- return merged;
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);
1289
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);
1290
1309
  }
1291
- 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)
1292
- 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 (?, ?, ?, ?, ?, ?, ?, ?)`, [
1293
1313
  id,
1294
- input.key,
1295
- input.value,
1296
- input.category || "knowledge",
1297
- input.scope || "private",
1298
- input.summary || null,
1299
- tagsJson,
1300
- input.importance ?? 5,
1301
- input.source || "agent",
1302
- input.agent_id || null,
1303
- input.project_id || null,
1304
- input.session_id || null,
1314
+ input.name,
1315
+ input.type,
1316
+ input.description || null,
1305
1317
  metadataJson,
1306
- expiresAt,
1318
+ input.project_id || null,
1307
1319
  timestamp,
1308
1320
  timestamp
1309
1321
  ]);
1310
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1311
- for (const tag of tags) {
1312
- insertTag.run(id, tag);
1313
- }
1314
- const memory = getMemory(id, d);
1315
- try {
1316
- runEntityExtraction(memory, input.project_id, d);
1317
- } catch {}
1318
- return memory;
1322
+ return getEntity(id, d);
1319
1323
  }
1320
- function getMemory(id, db) {
1324
+ function getEntity(id, db) {
1321
1325
  const d = db || getDatabase();
1322
- const row = d.query("SELECT * FROM memories WHERE id = ?").get(id);
1326
+ const row = d.query("SELECT * FROM entities WHERE id = ?").get(id);
1323
1327
  if (!row)
1324
- return null;
1325
- return parseMemoryRow(row);
1328
+ throw new EntityNotFoundError(id);
1329
+ return parseEntityRow2(row);
1326
1330
  }
1327
- function getMemoryByKey(key, scope, agentId, projectId, sessionId, db) {
1331
+ function getEntityByName(name, type, projectId, db) {
1328
1332
  const d = db || getDatabase();
1329
- let sql = "SELECT * FROM memories WHERE key = ?";
1330
- const params = [key];
1331
- if (scope) {
1332
- sql += " AND scope = ?";
1333
- params.push(scope);
1334
- }
1335
- if (agentId) {
1336
- sql += " AND agent_id = ?";
1337
- 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);
1338
1338
  }
1339
- if (projectId) {
1339
+ if (projectId !== undefined) {
1340
1340
  sql += " AND project_id = ?";
1341
1341
  params.push(projectId);
1342
1342
  }
1343
- if (sessionId) {
1344
- sql += " AND session_id = ?";
1345
- params.push(sessionId);
1346
- }
1347
- sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
1343
+ sql += " LIMIT 1";
1348
1344
  const row = d.query(sql).get(...params);
1349
1345
  if (!row)
1350
1346
  return null;
1351
- return parseMemoryRow(row);
1347
+ return parseEntityRow2(row);
1352
1348
  }
1353
- function getMemoriesByKey(key, scope, agentId, projectId, db) {
1349
+ function listEntities(filter = {}, db) {
1354
1350
  const d = db || getDatabase();
1355
- let sql = "SELECT * FROM memories WHERE key = ?";
1356
- const params = [key];
1357
- if (scope) {
1358
- sql += " AND scope = ?";
1359
- params.push(scope);
1351
+ const conditions = [];
1352
+ const params = [];
1353
+ if (filter.type) {
1354
+ conditions.push("type = ?");
1355
+ params.push(filter.type);
1360
1356
  }
1361
- if (agentId) {
1362
- sql += " AND agent_id = ?";
1363
- params.push(agentId);
1357
+ if (filter.project_id) {
1358
+ conditions.push("project_id = ?");
1359
+ params.push(filter.project_id);
1364
1360
  }
1365
- if (projectId) {
1366
- sql += " AND project_id = ?";
1367
- params.push(projectId);
1368
- }
1369
- sql += " AND status = 'active' ORDER BY importance DESC";
1370
- const rows = d.query(sql).all(...params);
1371
- return rows.map(parseMemoryRow);
1372
- }
1373
- function listMemories(filter, db) {
1374
- const d = db || getDatabase();
1375
- const conditions = [];
1376
- const params = [];
1377
- if (filter) {
1378
- if (filter.scope) {
1379
- if (Array.isArray(filter.scope)) {
1380
- conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
1381
- params.push(...filter.scope);
1382
- } else {
1383
- conditions.push("scope = ?");
1384
- params.push(filter.scope);
1385
- }
1386
- }
1387
- if (filter.category) {
1388
- if (Array.isArray(filter.category)) {
1389
- conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
1390
- params.push(...filter.category);
1391
- } else {
1392
- conditions.push("category = ?");
1393
- params.push(filter.category);
1394
- }
1395
- }
1396
- if (filter.source) {
1397
- if (Array.isArray(filter.source)) {
1398
- conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
1399
- params.push(...filter.source);
1400
- } else {
1401
- conditions.push("source = ?");
1402
- params.push(filter.source);
1403
- }
1404
- }
1405
- if (filter.status) {
1406
- if (Array.isArray(filter.status)) {
1407
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1408
- params.push(...filter.status);
1409
- } else {
1410
- conditions.push("status = ?");
1411
- params.push(filter.status);
1412
- }
1413
- } else {
1414
- conditions.push("status = 'active'");
1415
- }
1416
- if (filter.project_id) {
1417
- conditions.push("project_id = ?");
1418
- params.push(filter.project_id);
1419
- }
1420
- if (filter.agent_id) {
1421
- conditions.push("agent_id = ?");
1422
- params.push(filter.agent_id);
1423
- }
1424
- if (filter.session_id) {
1425
- conditions.push("session_id = ?");
1426
- params.push(filter.session_id);
1427
- }
1428
- if (filter.min_importance) {
1429
- conditions.push("importance >= ?");
1430
- params.push(filter.min_importance);
1431
- }
1432
- if (filter.pinned !== undefined) {
1433
- conditions.push("pinned = ?");
1434
- params.push(filter.pinned ? 1 : 0);
1435
- }
1436
- if (filter.tags && filter.tags.length > 0) {
1437
- for (const tag of filter.tags) {
1438
- conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
1439
- params.push(tag);
1440
- }
1441
- }
1442
- if (filter.search) {
1443
- conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
1444
- const term = `%${filter.search}%`;
1445
- params.push(term, term, term);
1446
- }
1447
- } else {
1448
- conditions.push("status = 'active'");
1361
+ if (filter.search) {
1362
+ conditions.push("(name LIKE ? OR description LIKE ?)");
1363
+ const term = `%${filter.search}%`;
1364
+ params.push(term, term);
1449
1365
  }
1450
- let sql = "SELECT * FROM memories";
1366
+ let sql = "SELECT * FROM entities";
1451
1367
  if (conditions.length > 0) {
1452
1368
  sql += ` WHERE ${conditions.join(" AND ")}`;
1453
1369
  }
1454
- sql += " ORDER BY importance DESC, created_at DESC";
1455
- if (filter?.limit) {
1370
+ sql += " ORDER BY updated_at DESC";
1371
+ if (filter.limit) {
1456
1372
  sql += " LIMIT ?";
1457
1373
  params.push(filter.limit);
1458
1374
  }
1459
- if (filter?.offset) {
1375
+ if (filter.offset) {
1460
1376
  sql += " OFFSET ?";
1461
1377
  params.push(filter.offset);
1462
1378
  }
1463
1379
  const rows = d.query(sql).all(...params);
1464
- return rows.map(parseMemoryRow);
1380
+ return rows.map(parseEntityRow2);
1465
1381
  }
1466
- function updateMemory(id, input, db) {
1382
+ function updateEntity(id, input, db) {
1467
1383
  const d = db || getDatabase();
1468
- const existing = getMemory(id, d);
1384
+ const existing = d.query("SELECT id FROM entities WHERE id = ?").get(id);
1469
1385
  if (!existing)
1470
- throw new MemoryNotFoundError(id);
1471
- if (existing.version !== input.version) {
1472
- throw new VersionConflictError(id, input.version, existing.version);
1473
- }
1474
- try {
1475
- d.run(`INSERT OR IGNORE INTO memory_versions (id, memory_id, version, value, importance, scope, category, tags, summary, pinned, status, created_at)
1476
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1477
- uuid(),
1478
- existing.id,
1479
- existing.version,
1480
- existing.value,
1481
- existing.importance,
1482
- existing.scope,
1483
- existing.category,
1484
- JSON.stringify(existing.tags),
1485
- existing.summary,
1486
- existing.pinned ? 1 : 0,
1487
- existing.status,
1488
- existing.updated_at
1489
- ]);
1490
- } catch {}
1491
- const sets = ["version = version + 1", "updated_at = ?"];
1386
+ throw new EntityNotFoundError(id);
1387
+ const sets = ["updated_at = ?"];
1492
1388
  const params = [now()];
1493
- if (input.value !== undefined) {
1494
- sets.push("value = ?");
1495
- params.push(redactSecrets(input.value));
1496
- }
1497
- if (input.category !== undefined) {
1498
- sets.push("category = ?");
1499
- params.push(input.category);
1500
- }
1501
- if (input.scope !== undefined) {
1502
- sets.push("scope = ?");
1503
- params.push(input.scope);
1504
- }
1505
- if (input.summary !== undefined) {
1506
- sets.push("summary = ?");
1507
- params.push(input.summary);
1508
- }
1509
- if (input.importance !== undefined) {
1510
- sets.push("importance = ?");
1511
- params.push(input.importance);
1389
+ if (input.name !== undefined) {
1390
+ sets.push("name = ?");
1391
+ params.push(input.name);
1512
1392
  }
1513
- if (input.pinned !== undefined) {
1514
- sets.push("pinned = ?");
1515
- params.push(input.pinned ? 1 : 0);
1393
+ if (input.type !== undefined) {
1394
+ sets.push("type = ?");
1395
+ params.push(input.type);
1516
1396
  }
1517
- if (input.status !== undefined) {
1518
- sets.push("status = ?");
1519
- params.push(input.status);
1397
+ if (input.description !== undefined) {
1398
+ sets.push("description = ?");
1399
+ params.push(input.description);
1520
1400
  }
1521
1401
  if (input.metadata !== undefined) {
1522
1402
  sets.push("metadata = ?");
1523
1403
  params.push(JSON.stringify(input.metadata));
1524
1404
  }
1525
- if (input.expires_at !== undefined) {
1526
- sets.push("expires_at = ?");
1527
- params.push(input.expires_at);
1528
- }
1529
- if (input.tags !== undefined) {
1530
- sets.push("tags = ?");
1531
- params.push(JSON.stringify(input.tags));
1532
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
1533
- const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
1534
- for (const tag of input.tags) {
1535
- insertTag.run(id, tag);
1536
- }
1537
- }
1538
1405
  params.push(id);
1539
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
1540
- const updated = getMemory(id, d);
1541
- try {
1542
- if (input.value !== undefined) {
1543
- const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
1544
- for (const link of oldLinks) {
1545
- unlinkEntityFromMemory(link.entity_id, updated.id, d);
1546
- }
1547
- runEntityExtraction(updated, existing.project_id || undefined, d);
1548
- }
1549
- } catch {}
1550
- return updated;
1551
- }
1552
- function deleteMemory(id, db) {
1553
- const d = db || getDatabase();
1554
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
1555
- return result.changes > 0;
1556
- }
1557
- function bulkDeleteMemories(ids, db) {
1558
- const d = db || getDatabase();
1559
- if (ids.length === 0)
1560
- return 0;
1561
- const placeholders = ids.map(() => "?").join(",");
1562
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
1563
- const count = countRow.c;
1564
- if (count > 0) {
1565
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
1566
- }
1567
- return count;
1568
- }
1569
- function touchMemory(id, db) {
1570
- const d = db || getDatabase();
1571
- 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);
1572
1408
  }
1573
- function cleanExpiredMemories(db) {
1409
+ function deleteEntity(id, db) {
1574
1410
  const d = db || getDatabase();
1575
- const timestamp = now();
1576
- const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
1577
- const count = countRow.c;
1578
- if (count > 0) {
1579
- d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
1580
- }
1581
- return count;
1411
+ const result = d.run("DELETE FROM entities WHERE id = ?", [id]);
1412
+ if (result.changes === 0)
1413
+ throw new EntityNotFoundError(id);
1582
1414
  }
1583
- function getMemoryVersions(memoryId, db) {
1415
+ function mergeEntities(sourceId, targetId, db) {
1584
1416
  const d = db || getDatabase();
1585
- try {
1586
- const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
1587
- return rows.map((row) => ({
1588
- id: row["id"],
1589
- memory_id: row["memory_id"],
1590
- version: row["version"],
1591
- value: row["value"],
1592
- importance: row["importance"],
1593
- scope: row["scope"],
1594
- category: row["category"],
1595
- tags: JSON.parse(row["tags"] || "[]"),
1596
- summary: row["summary"] || null,
1597
- pinned: !!row["pinned"],
1598
- status: row["status"],
1599
- created_at: row["created_at"]
1600
- }));
1601
- } catch {
1602
- return [];
1603
- }
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);
1604
1430
  }
1431
+
1605
1432
  // src/lib/search.ts
1606
1433
  function parseMemoryRow2(row) {
1607
1434
  return {
@@ -2126,6 +1953,12 @@ function searchMemories(query, filter, db) {
2126
1953
  const offset = filter?.offset ?? 0;
2127
1954
  const limit = filter?.limit ?? scored.length;
2128
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
+ }
2129
1962
  logSearchQuery(query, scored.length, filter?.agent_id, filter?.project_id, d);
2130
1963
  return finalResults;
2131
1964
  }
@@ -2136,6 +1969,148 @@ function logSearchQuery(query, resultCount, agentId, projectId, db) {
2136
1969
  d.run("INSERT INTO search_history (id, query, result_count, agent_id, project_id) VALUES (?, ?, ?, ?, ?)", [id, query, resultCount, agentId || null, projectId || null]);
2137
1970
  } catch {}
2138
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
+ }
2139
2114
  // src/lib/injector.ts
2140
2115
  class MemoryInjector {
2141
2116
  config;
@@ -2480,28 +2455,940 @@ function syncMemories(agentName, direction = "both", options = {}) {
2480
2455
  return result;
2481
2456
  }
2482
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
+ }
2893
+ }
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
+ });
2914
+ }
2915
+ }
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
+ });
2935
+ }
2936
+ }
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
+ });
2956
+ }
2957
+ }
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
+ }
3023
+ }
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
+ }
3038
+ }
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 });
3055
+ }
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
+ }
3129
+ }
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;
3160
+ }
3161
+ }
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);
3198
+ }
3199
+ }
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;
3208
+ }
3209
+ try {
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()
3228
+ }
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;
3235
+ }
3236
+ }
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
3247
+ };
3248
+ let extracted = [];
3249
+ try {
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
+ }
3261
+ }
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);
3271
+ }
3272
+ }
3273
+ }
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
+ }
2483
3359
  export {
3360
+ withMemoryLock,
2484
3361
  uuid,
2485
3362
  updateMemory,
2486
3363
  updateEntity,
2487
3364
  updateAgent,
2488
3365
  unlinkEntityFromMemory,
3366
+ unfocus,
2489
3367
  touchMemory,
2490
3368
  touchAgent,
2491
3369
  syncMemories,
2492
3370
  shortUuid,
3371
+ setFocus,
2493
3372
  setActiveProfile,
2494
3373
  searchMemories,
2495
3374
  runCleanup,
3375
+ resolveProjectId,
2496
3376
  resolvePartialId,
2497
3377
  resetDatabase,
3378
+ releaseResourceLocks,
3379
+ releaseMemoryWriteLock,
3380
+ releaseLock,
3381
+ releaseAllAgentLocks,
2498
3382
  registerProject,
2499
3383
  registerAgent,
2500
3384
  redactSecrets,
3385
+ providerRegistry,
3386
+ processConversationTurn,
2501
3387
  parseRelationRow,
2502
- parseEntityRow,
3388
+ parseEntityRow2 as parseEntityRow,
2503
3389
  now,
2504
3390
  mergeEntities,
3391
+ memoryLockId,
2505
3392
  loadConfig,
2506
3393
  listRelations,
2507
3394
  listProjects,
@@ -2510,7 +3397,9 @@ export {
2510
3397
  listEntities,
2511
3398
  listAgentsByProject,
2512
3399
  listAgents,
3400
+ listAgentLocks,
2513
3401
  linkEntityToMemory,
3402
+ incrementRecallCount,
2514
3403
  getRelation,
2515
3404
  getRelatedEntities,
2516
3405
  getProject,
@@ -2519,17 +3408,20 @@ export {
2519
3408
  getMemory,
2520
3409
  getMemoriesForEntity,
2521
3410
  getMemoriesByKey,
3411
+ getFocus,
2522
3412
  getEntityMemoryLinks,
2523
3413
  getEntityGraph,
2524
3414
  getEntityByName,
2525
3415
  getEntity,
2526
3416
  getEntitiesForMemory,
3417
+ getDedupStats,
2527
3418
  getDbPath,
2528
3419
  getDatabase,
3420
+ getAutoMemoryStats,
2529
3421
  getAgent,
2530
3422
  getActiveProfile,
3423
+ focusFilterSQL,
2531
3424
  findPath,
2532
- extractEntities,
2533
3425
  enforceQuotas,
2534
3426
  deprioritizeStale,
2535
3427
  deleteRelation,
@@ -2537,18 +3429,28 @@ export {
2537
3429
  deleteMemory,
2538
3430
  deleteEntity,
2539
3431
  defaultSyncAgents,
3432
+ dedup,
2540
3433
  createRelation,
2541
3434
  createMemory,
2542
3435
  createEntity,
2543
3436
  containsSecrets,
3437
+ configureAutoMemory,
2544
3438
  closeDatabase,
2545
3439
  cleanExpiredMemories,
3440
+ cleanExpiredLocks,
3441
+ checkMemoryWriteLock,
3442
+ checkLock,
2546
3443
  bulkLinkEntities,
2547
3444
  bulkDeleteMemories,
3445
+ buildFocusFilter,
2548
3446
  archiveUnused,
2549
3447
  archiveStale,
3448
+ agentHoldsLock,
3449
+ acquireMemoryWriteLock,
3450
+ acquireLock,
2550
3451
  VersionConflictError,
2551
3452
  MemoryNotFoundError,
3453
+ MemoryLockConflictError,
2552
3454
  MemoryInjector,
2553
3455
  MemoryExpiredError,
2554
3456
  InvalidScopeError,