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