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