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