persyst-mcp 2.2.4 → 2.2.6
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/README.md +64 -2
- package/bin/export.js +116 -0
- package/bin/import.js +160 -0
- package/bin/init.js +168 -32
- package/bin/mcp.js +7 -0
- package/hooks/persyst-hook.js +9 -10
- package/index.js +42 -12
- package/package.json +15 -10
- package/src/attestation.js +49 -28
- package/src/database.js +229 -36
- package/src/events.js +19 -0
- package/src/extractor-heuristic.js +505 -324
- package/src/sdk.d.ts +175 -0
- package/src/sdk.js +218 -0
- package/src/search.js +144 -83
- package/src/server.js +766 -93
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +41 -0
- package/src/tools.js +58 -46
- package/src/watcher.js +174 -50
package/src/database.js
CHANGED
|
@@ -34,6 +34,9 @@ const db = new Database(DB_PATH);
|
|
|
34
34
|
db.pragma('journal_mode = WAL'); // Better performance for concurrent reads
|
|
35
35
|
db.pragma('foreign_keys = ON'); // Enforce referential integrity
|
|
36
36
|
db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O for faster reads
|
|
37
|
+
db.pragma('synchronous = NORMAL'); // Performance boost for WAL mode
|
|
38
|
+
db.pragma('temp_store = MEMORY'); // Keep temp tables in memory
|
|
39
|
+
db.pragma('cache_size = -64000'); // 64MB cache size
|
|
37
40
|
|
|
38
41
|
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
39
42
|
sqliteVec.load(db);
|
|
@@ -60,27 +63,32 @@ db.exec(`
|
|
|
60
63
|
`);
|
|
61
64
|
|
|
62
65
|
// --- Migrations for bi-temporal validity on existing tables ---
|
|
63
|
-
|
|
66
|
+
function columnExists(table, name) {
|
|
67
|
+
const info = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
68
|
+
return info.some(col => col.name === name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!columnExists('memories', 'valid_from')) {
|
|
64
72
|
db.exec('ALTER TABLE memories ADD COLUMN valid_from INTEGER DEFAULT (unixepoch())');
|
|
65
|
-
}
|
|
73
|
+
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
if (!columnExists('memories', 'valid_until')) {
|
|
68
76
|
db.exec('ALTER TABLE memories ADD COLUMN valid_until INTEGER DEFAULT NULL');
|
|
69
|
-
}
|
|
77
|
+
}
|
|
70
78
|
|
|
71
|
-
|
|
79
|
+
if (!columnExists('memories', 'assertion_time')) {
|
|
72
80
|
db.exec('ALTER TABLE memories ADD COLUMN assertion_time INTEGER DEFAULT (unixepoch())');
|
|
73
|
-
}
|
|
81
|
+
}
|
|
74
82
|
|
|
75
83
|
// --- Migration: add namespace column for per-agent isolation ---
|
|
76
|
-
|
|
84
|
+
if (!columnExists('memories', 'namespace')) {
|
|
77
85
|
db.exec("ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'shared'");
|
|
78
|
-
}
|
|
86
|
+
}
|
|
79
87
|
|
|
80
88
|
// --- Migration: add parent_id column for history tracing ---
|
|
81
|
-
|
|
89
|
+
if (!columnExists('memories', 'parent_id')) {
|
|
82
90
|
db.exec('ALTER TABLE memories ADD COLUMN parent_id INTEGER DEFAULT NULL');
|
|
83
|
-
}
|
|
91
|
+
}
|
|
84
92
|
|
|
85
93
|
// --- Index on namespace for fast filtered queries ---
|
|
86
94
|
try {
|
|
@@ -330,7 +338,7 @@ const stmts = {
|
|
|
330
338
|
decay: db.prepare(`
|
|
331
339
|
UPDATE memories
|
|
332
340
|
SET importance_score = ROUND(MAX(importance_score * 0.95, 0.0), 4)
|
|
333
|
-
WHERE (
|
|
341
|
+
WHERE (unixepoch() - last_accessed) > 604800
|
|
334
342
|
`),
|
|
335
343
|
|
|
336
344
|
// -- Search --
|
|
@@ -364,6 +372,11 @@ const stmts = {
|
|
|
364
372
|
deleteEntity: db.prepare(
|
|
365
373
|
'DELETE FROM entities WHERE id = ?'
|
|
366
374
|
),
|
|
375
|
+
deleteEdgesByEntity: db.prepare(
|
|
376
|
+
`DELETE FROM edges WHERE
|
|
377
|
+
(source_id = ? AND source_type = 'entity') OR
|
|
378
|
+
(target_id = ? AND target_type = 'entity')`
|
|
379
|
+
),
|
|
367
380
|
|
|
368
381
|
// -- Edges --
|
|
369
382
|
insertEdge: db.prepare(
|
|
@@ -423,9 +436,171 @@ const stmts = {
|
|
|
423
436
|
INSERT INTO watched_files (file_path, last_position)
|
|
424
437
|
VALUES (?, ?)
|
|
425
438
|
ON CONFLICT(file_path) DO UPDATE SET last_position = excluded.last_position, updated_at = unixepoch()
|
|
439
|
+
`),
|
|
440
|
+
|
|
441
|
+
// -- Internal lookups (pre-compiled for hot-loop use) --
|
|
442
|
+
getAttestationByHash: db.prepare(
|
|
443
|
+
'SELECT * FROM attestations WHERE hash = ?'
|
|
444
|
+
),
|
|
445
|
+
getMemoryParentId: db.prepare(
|
|
446
|
+
'SELECT parent_id FROM memories WHERE id = ?'
|
|
447
|
+
),
|
|
448
|
+
getMemoryChildren: db.prepare(
|
|
449
|
+
'SELECT id FROM memories WHERE parent_id = ?'
|
|
450
|
+
),
|
|
451
|
+
getMemoryContentById: db.prepare(
|
|
452
|
+
'SELECT content FROM memories WHERE id = ?'
|
|
453
|
+
),
|
|
454
|
+
getMemoryByIdRaw: db.prepare(
|
|
455
|
+
'SELECT * FROM memories WHERE id = ? AND valid_until IS NULL'
|
|
456
|
+
),
|
|
457
|
+
getMemoryLikeContent: db.prepare(
|
|
458
|
+
'SELECT id FROM memories WHERE content LIKE ? AND valid_until IS NULL'
|
|
459
|
+
),
|
|
460
|
+
getVecByRowId: db.prepare(
|
|
461
|
+
'SELECT embedding FROM memories_vec WHERE rowid = ?'
|
|
462
|
+
),
|
|
463
|
+
updateMemoryParentId: db.prepare(
|
|
464
|
+
'UPDATE memories SET parent_id = ? WHERE id = ?'
|
|
465
|
+
),
|
|
466
|
+
deleteProvenanceByMemoryId: db.prepare(
|
|
467
|
+
'DELETE FROM provenance WHERE memory_id = ?'
|
|
468
|
+
),
|
|
469
|
+
deleteContradictionsByMemoryId: db.prepare(
|
|
470
|
+
'DELETE FROM contradictions WHERE old_memory_id = ? OR new_memory_id = ?'
|
|
471
|
+
),
|
|
472
|
+
getReputationScore: db.prepare(
|
|
473
|
+
'SELECT reputation_score FROM agent_stats WHERE agent_id = ?'
|
|
474
|
+
),
|
|
475
|
+
updateProvenanceOwner: db.prepare(
|
|
476
|
+
"UPDATE provenance SET source_type = 'agent', source_id = ?, confidence = 1.0 WHERE memory_id = ?"
|
|
477
|
+
),
|
|
478
|
+
archiveMemoryById: db.prepare(
|
|
479
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
480
|
+
),
|
|
481
|
+
getEdgesBySourceAndType: db.prepare(`
|
|
482
|
+
SELECT * FROM edges
|
|
483
|
+
WHERE (source_id = ? AND source_type = ?)
|
|
484
|
+
OR (target_id = ? AND target_type = ?)
|
|
485
|
+
`),
|
|
486
|
+
getMemoriesByEntityEdges: db.prepare(`
|
|
487
|
+
SELECT * FROM edges
|
|
488
|
+
WHERE (source_id = ? AND source_type = 'entity' AND target_type = 'memory')
|
|
489
|
+
OR (target_id = ? AND target_type = 'entity' AND source_type = 'memory')
|
|
490
|
+
`),
|
|
491
|
+
consolidateVecSearch: db.prepare(`
|
|
492
|
+
SELECT rowid AS id, distance
|
|
493
|
+
FROM memories_vec
|
|
494
|
+
WHERE embedding MATCH ?
|
|
495
|
+
AND k = 30
|
|
496
|
+
`),
|
|
497
|
+
archiveAndInsertContradiction: db.prepare(
|
|
498
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
499
|
+
),
|
|
500
|
+
archiveExpiredTransientMemories: db.prepare(`
|
|
501
|
+
UPDATE memories
|
|
502
|
+
SET valid_until = unixepoch()
|
|
503
|
+
WHERE valid_until IS NULL
|
|
504
|
+
AND (content LIKE 'Reminder:%' OR content LIKE 'Note:%')
|
|
505
|
+
AND (unixepoch() - created_at) > 1209600
|
|
426
506
|
`)
|
|
427
507
|
};
|
|
428
508
|
|
|
509
|
+
export { stmts };
|
|
510
|
+
|
|
511
|
+
// ============================================================
|
|
512
|
+
// SECRET DETECTION & REDACTION HELPERS
|
|
513
|
+
// ============================================================
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Detects sensitive/credential patterns in a string and replaces values with [REDACTED].
|
|
517
|
+
* @param {string} content - The content to sanitize
|
|
518
|
+
* @returns {string} Sanitized content
|
|
519
|
+
*/
|
|
520
|
+
export function redactSecrets(content) {
|
|
521
|
+
if (!content || typeof content !== 'string') return content;
|
|
522
|
+
|
|
523
|
+
let redacted = content;
|
|
524
|
+
|
|
525
|
+
// 1. Redact credentials in connection strings / URIs
|
|
526
|
+
// Matches scheme://user:pass@host and scheme://:pass@host
|
|
527
|
+
const connectionStringRegex = /\b([a-zA-Z0-9+.-]+:\/\/)([^/:\s]*):([^@/:\s]+)(@[^/\s]+)/gi;
|
|
528
|
+
redacted = redacted.replace(connectionStringRegex, (match, protocol, user, pass, host) => {
|
|
529
|
+
return protocol + user + ':[REDACTED]' + host;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// 2. Redact key-value pairs matching credentials (retaining key/operator, redacting value)
|
|
533
|
+
// Supports single-quoted, double-quoted, and unquoted values (non-whitespace).
|
|
534
|
+
const kvRegex = /['"]?\b(api[_-]?key|secret[_-]?key|secret|password|passwd|pwd|passphrase|auth[_-]?token|access[_-]?token|client[_-]?secret|private[_-]?key|auth|access|client|aws|gcp|google|stripe|github|openai|vercel|heroku|slack|ssh[_-]?(?:key|password|passphrase|pass)?|credential|aws_secret|secret_access_key|aws_access_key|ssh_passphrase|ssh_password|ssh_key_pass)\b['"]?\s*(?:key|token|secret|password|pwd|passwd|value|string|id)?(?:\b|(?<=['"]))\s*([:=]|is|of|to|set\s+to|\(|\buses\b)\s*(?:'([^']{6,2048})'|"([^"]{6,2048})"|([^\s]+(?:\n(?![a-zA-Z0-9_-]+\s*[:=])(?=[^\s]+(?:\n|$))[^\s]+)*))/gi;
|
|
535
|
+
|
|
536
|
+
redacted = redacted.replace(kvRegex, (match, key, op, sqVal, dqVal, uqVal) => {
|
|
537
|
+
const val = sqVal || dqVal || uqVal;
|
|
538
|
+
if (!val) return match;
|
|
539
|
+
|
|
540
|
+
// Strip trailing parenthesis if operator is '(' and value has trailing parenthesis
|
|
541
|
+
let cleanVal = val;
|
|
542
|
+
if (op === '(' && val.endsWith(')')) {
|
|
543
|
+
cleanVal = val.slice(0, -1);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const lastIdx = match.lastIndexOf(cleanVal);
|
|
547
|
+
if (lastIdx !== -1) {
|
|
548
|
+
return match.slice(0, lastIdx) + '[REDACTED]' + match.slice(lastIdx + cleanVal.length);
|
|
549
|
+
}
|
|
550
|
+
return match;
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// 3. Redact standalone common API keys and tokens
|
|
554
|
+
const standalonePatterns = [
|
|
555
|
+
/\b(sk-[a-zA-Z0-9]{48})\b/g, // OpenAI
|
|
556
|
+
/\b(sk-proj-[a-zA-Z0-9-]{40,})\b/g, // OpenAI project
|
|
557
|
+
/\b(gh[pous]_[a-zA-Z0-9]{36,255})\b/g, // GitHub PAT/Fine-grained
|
|
558
|
+
/\b(xox[bapr]-[0-9]{12}-[a-zA-Z0-9]{24})\b/g, // Slack token
|
|
559
|
+
/\b(AIzaSy[A-Za-z0-9_-]{33})\b/g, // Google API key
|
|
560
|
+
/\b((?:sk|rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,32})\b/g, // Stripe key
|
|
561
|
+
/\b(AKIA[0-9A-Z]{16,40})\b/gi, // AWS Access Key ID (case-insensitive)
|
|
562
|
+
/\b(ASCA[0-9A-Z]{16,40})\b/gi, // AWS ASCA Key
|
|
563
|
+
/\b(npm_[a-zA-Z0-9]{36,255})\b/g, // npm token
|
|
564
|
+
/\b(ey[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,})\b/g, // JWT token
|
|
565
|
+
/-----BEGIN[A-Z0-9\s_-]+PRIVATE\s+KEY[A-Z0-9\s_-]*-----\s*[\s\S]*?-----END[A-Z0-9\s_-]+PRIVATE\s+KEY[A-Z0-9\s_-]*-----/gi, // PEM private key
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
for (const pattern of standalonePatterns) {
|
|
569
|
+
redacted = redacted.replace(pattern, '[REDACTED]');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 4. Robust credential shape heuristic (independent of strict key-value punctuation)
|
|
573
|
+
const containsCredKeyword = /\b(password|passwd|pwd|passphrase|pass|secret|token|api|credential|auth|ssh|aws)\b/i.test(redacted);
|
|
574
|
+
if (containsCredKeyword) {
|
|
575
|
+
// Match password-like tokens: length 6 to 64, containing both letters and digits/symbols
|
|
576
|
+
const tokenRegex = /\b([a-zA-Z0-9_@#$%^&*+!~\-]{6,64})(?!\w)/g;
|
|
577
|
+
|
|
578
|
+
redacted = redacted.replace(tokenRegex, (match) => {
|
|
579
|
+
// Skip common query words and technical keywords
|
|
580
|
+
if (/^(password|passwd|pwd|passphrase|pass|secret|token|api|credential|auth|ssh|uses|with|key|from|here|what|have|this|that|your|same|then|want|more|base64|base64-like|base64-encoded|sha256|sha1|md5|aes256|aes128|utf8|utf-8|url|uri|ipv4|ipv6|http|https|sha-256|sha-1)$/i.test(match)) {
|
|
581
|
+
return match;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const hasLetter = /[a-zA-Z]/.test(match);
|
|
585
|
+
const hasDigitOrSpecialSymbol = /[0-9@#$%^&*+=!~]/.test(match);
|
|
586
|
+
|
|
587
|
+
if (hasLetter && hasDigitOrSpecialSymbol) {
|
|
588
|
+
// Require length >= 8 or containing special symbols/mixed case digits
|
|
589
|
+
const isStrongSecretCandidate = match.length >= 8 ||
|
|
590
|
+
(/[^a-zA-Z0-9_]/.test(match)) ||
|
|
591
|
+
(/[A-Z]/.test(match) && /[0-9]/.test(match));
|
|
592
|
+
|
|
593
|
+
if (isStrongSecretCandidate) {
|
|
594
|
+
return '[REDACTED]';
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return match;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return redacted;
|
|
602
|
+
}
|
|
603
|
+
|
|
429
604
|
// ============================================================
|
|
430
605
|
// CRUD FUNCTIONS
|
|
431
606
|
// Simple, one-purpose functions. No magic.
|
|
@@ -440,11 +615,12 @@ const stmts = {
|
|
|
440
615
|
* @returns {number} The new memory's ID
|
|
441
616
|
*/
|
|
442
617
|
export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared', parentId = null) {
|
|
443
|
-
|
|
618
|
+
const redactedContent = redactSecrets(content);
|
|
619
|
+
if (redactedContent && redactedContent.length > 10000) {
|
|
444
620
|
throw new Error('Memory content exceeds maximum length of 10000 characters.');
|
|
445
621
|
}
|
|
446
622
|
const clampedImportance = Math.max(0.0, Math.min(1.0, Math.round(importance * 10000) / 10000));
|
|
447
|
-
const result = stmts.insertMemory.run(
|
|
623
|
+
const result = stmts.insertMemory.run(redactedContent, clampedImportance, namespace || 'shared', parentId);
|
|
448
624
|
const id = Number(result.lastInsertRowid);
|
|
449
625
|
|
|
450
626
|
// Provenance Info handling
|
|
@@ -481,13 +657,12 @@ export function insertVector(id, embedding) {
|
|
|
481
657
|
* @returns {object|null} The memory row, or null if not found
|
|
482
658
|
*/
|
|
483
659
|
export function getMemory(id, namespace = null) {
|
|
484
|
-
const memory = namespace
|
|
485
|
-
? stmts.
|
|
486
|
-
: stmts.
|
|
660
|
+
const memory = (namespace === 'all' || namespace === null)
|
|
661
|
+
? stmts.getById.get(id)
|
|
662
|
+
: stmts.getByIdNs.get(id, namespace);
|
|
487
663
|
if (memory) {
|
|
488
664
|
boostMemory(id);
|
|
489
|
-
|
|
490
|
-
memory.provenance = prov;
|
|
665
|
+
memory.provenance = getProvenance(id);
|
|
491
666
|
}
|
|
492
667
|
return memory || null;
|
|
493
668
|
}
|
|
@@ -511,10 +686,9 @@ export function getAnyMemoryById(id) {
|
|
|
511
686
|
* @returns {object|null} The memory row, or null if not found
|
|
512
687
|
*/
|
|
513
688
|
export function getMemoryById(id, namespace = null) {
|
|
514
|
-
const
|
|
515
|
-
const memory = ns === 'all'
|
|
689
|
+
const memory = (namespace === 'all' || namespace === null)
|
|
516
690
|
? stmts.getById.get(id)
|
|
517
|
-
: stmts.getByIdNs.get(id,
|
|
691
|
+
: stmts.getByIdNs.get(id, namespace);
|
|
518
692
|
if (memory) {
|
|
519
693
|
memory.provenance = getProvenance(id);
|
|
520
694
|
}
|
|
@@ -527,7 +701,8 @@ export function getMemoryById(id, namespace = null) {
|
|
|
527
701
|
* @returns {boolean} true if the memory existed and was updated
|
|
528
702
|
*/
|
|
529
703
|
export function updateMemoryContent(id, content) {
|
|
530
|
-
const
|
|
704
|
+
const redactedContent = redactSecrets(content);
|
|
705
|
+
const result = stmts.updateContent.run(redactedContent, id);
|
|
531
706
|
return result.changes > 0;
|
|
532
707
|
}
|
|
533
708
|
|
|
@@ -547,8 +722,8 @@ export function deleteMemory(id) {
|
|
|
547
722
|
stmts.deleteEdgesByMemory.run(id, id);
|
|
548
723
|
deleteVec(id); // Remove vector first (no cascades on virtual tables)
|
|
549
724
|
try {
|
|
550
|
-
|
|
551
|
-
|
|
725
|
+
stmts.deleteProvenanceByMemoryId.run(id);
|
|
726
|
+
stmts.deleteContradictionsByMemoryId.run(id, id);
|
|
552
727
|
} catch (e) {
|
|
553
728
|
console.error(`[persyst] Clean up provenance/contradictions error: ${e.message}`);
|
|
554
729
|
}
|
|
@@ -615,8 +790,7 @@ export function boostMemory(id) {
|
|
|
615
790
|
* Called automatically every hour by the server.
|
|
616
791
|
*/
|
|
617
792
|
export function applyTemporalDecay() {
|
|
618
|
-
const
|
|
619
|
-
const result = stmts.decay.run(now);
|
|
793
|
+
const result = stmts.decay.run();
|
|
620
794
|
if (result.changes > 0) {
|
|
621
795
|
console.error(`[persyst] Decay applied to ${result.changes} memories`);
|
|
622
796
|
}
|
|
@@ -696,6 +870,7 @@ export function getAllEntities(limit = 50) {
|
|
|
696
870
|
* Delete an entity and its edges.
|
|
697
871
|
*/
|
|
698
872
|
export function deleteEntity(id) {
|
|
873
|
+
stmts.deleteEdgesByEntity.run(id, id);
|
|
699
874
|
stmts.deleteEntity.run(id);
|
|
700
875
|
}
|
|
701
876
|
|
|
@@ -710,11 +885,7 @@ export function insertEdge(sourceId, targetId, relation, sourceType, targetType)
|
|
|
710
885
|
* Get all memories linked to an entity.
|
|
711
886
|
*/
|
|
712
887
|
export function getMemoriesByEntity(entityId) {
|
|
713
|
-
const edges =
|
|
714
|
-
SELECT * FROM edges
|
|
715
|
-
WHERE (source_id = ? AND source_type = 'entity' AND target_type = 'memory')
|
|
716
|
-
OR (target_id = ? AND target_type = 'entity' AND source_type = 'memory')
|
|
717
|
-
`).all(entityId, entityId);
|
|
888
|
+
const edges = stmts.getMemoriesByEntityEdges.all(entityId, entityId);
|
|
718
889
|
const memoryIds = edges.map(e => e.source_type === 'memory' ? e.source_id : e.target_id);
|
|
719
890
|
return memoryIds.map(id => stmts.getById.get(id)).filter(Boolean);
|
|
720
891
|
}
|
|
@@ -749,7 +920,7 @@ export function memoryExistsByHashPrefix(pattern) {
|
|
|
749
920
|
* @returns {number}
|
|
750
921
|
*/
|
|
751
922
|
export function getActiveMemoryCount(namespace = null) {
|
|
752
|
-
if (namespace) {
|
|
923
|
+
if (namespace && namespace !== 'all') {
|
|
753
924
|
return stmts.getActiveMemoryCountNs.get(namespace).count;
|
|
754
925
|
}
|
|
755
926
|
return stmts.getActiveMemoryCount.get().count;
|
|
@@ -777,7 +948,7 @@ export function getMemoryByContent(content, namespace = null) {
|
|
|
777
948
|
const row = namespace
|
|
778
949
|
? stmts.findMemoryByContentNs.get(content, namespace)
|
|
779
950
|
: stmts.findMemoryByContent.get(content);
|
|
780
|
-
return row ? getMemoryById(row.id) : null;
|
|
951
|
+
return row ? getMemoryById(row.id, namespace) : null;
|
|
781
952
|
}
|
|
782
953
|
|
|
783
954
|
// ============================================================
|
|
@@ -795,7 +966,7 @@ export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
|
795
966
|
try {
|
|
796
967
|
const parentId = Math.min(oldMemoryId, newMemoryId);
|
|
797
968
|
const childId = Math.max(oldMemoryId, newMemoryId);
|
|
798
|
-
|
|
969
|
+
stmts.updateMemoryParentId.run(parentId, childId);
|
|
799
970
|
} catch (e) {
|
|
800
971
|
console.error(`[persyst] Failed to set parent_id on contradiction: ${e.message}`);
|
|
801
972
|
}
|
|
@@ -908,13 +1079,13 @@ export function getMemoryHistoryChain(memoryId) {
|
|
|
908
1079
|
versions.add(currentId);
|
|
909
1080
|
|
|
910
1081
|
// 1. Find parent (ancestor) from memories table
|
|
911
|
-
const row =
|
|
1082
|
+
const row = stmts.getMemoryParentId.get(currentId);
|
|
912
1083
|
if (row && row.parent_id !== null) {
|
|
913
1084
|
if (!versions.has(row.parent_id)) queue.push(row.parent_id);
|
|
914
1085
|
}
|
|
915
1086
|
|
|
916
1087
|
// 2. Find children (descendants) from memories table
|
|
917
|
-
const children =
|
|
1088
|
+
const children = stmts.getMemoryChildren.all(currentId);
|
|
918
1089
|
for (const child of children) {
|
|
919
1090
|
if (!versions.has(child.id)) queue.push(child.id);
|
|
920
1091
|
}
|
|
@@ -991,6 +1162,23 @@ export function upsertWatchPosition(filePath, position) {
|
|
|
991
1162
|
// CLEANUP
|
|
992
1163
|
// ============================================================
|
|
993
1164
|
|
|
1165
|
+
/**
|
|
1166
|
+
* Archive transient memories (reminders and notes) older than 14 days.
|
|
1167
|
+
* Returns the count of archived memories.
|
|
1168
|
+
*/
|
|
1169
|
+
export function archiveExpiredMemories() {
|
|
1170
|
+
try {
|
|
1171
|
+
const info = stmts.archiveExpiredTransientMemories.run();
|
|
1172
|
+
if (info.changes > 0) {
|
|
1173
|
+
console.error(`[persyst] Archived ${info.changes} expired transient memories (Note/Reminder older than 14 days).`);
|
|
1174
|
+
}
|
|
1175
|
+
return info.changes;
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
console.error(`[persyst] Failed to archive expired memories: ${e.message}`);
|
|
1178
|
+
return 0;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
994
1182
|
/**
|
|
995
1183
|
* Close the database connection. Call on shutdown.
|
|
996
1184
|
*/
|
|
@@ -999,4 +1187,9 @@ export function closeDatabase() {
|
|
|
999
1187
|
console.error('[persyst] Database closed');
|
|
1000
1188
|
}
|
|
1001
1189
|
|
|
1190
|
+
// Run auto-expiry cleanup on database startup to prune transient bloat immediately
|
|
1191
|
+
try {
|
|
1192
|
+
archiveExpiredMemories();
|
|
1193
|
+
} catch (_) {}
|
|
1194
|
+
|
|
1002
1195
|
export default db;
|
package/src/events.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* events.js — Persyst In-Process Memory Event Bus
|
|
3
|
+
*
|
|
4
|
+
* A shared EventEmitter used by the HTTP gateway (SSE broadcasting),
|
|
5
|
+
* the log watcher, and the MCP tool handlers to signal memory changes
|
|
6
|
+
* in real-time without tight coupling.
|
|
7
|
+
*
|
|
8
|
+
* Events emitted:
|
|
9
|
+
* memory_added { id, content, namespace, source }
|
|
10
|
+
* memory_deleted { id }
|
|
11
|
+
* memories_consolidated { consolidated_groups, details }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
|
|
16
|
+
export const memoryEventBus = new EventEmitter();
|
|
17
|
+
|
|
18
|
+
// Support large swarms with many simultaneous SSE subscribers
|
|
19
|
+
memoryEventBus.setMaxListeners(500);
|