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/search.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import db, {
|
|
11
|
+
stmts,
|
|
11
12
|
searchKeyword,
|
|
12
13
|
searchVector,
|
|
13
14
|
getMemoryById,
|
|
@@ -19,6 +20,7 @@ import db, {
|
|
|
19
20
|
import { generateEmbedding } from './embeddings.js';
|
|
20
21
|
import { createAttestation } from './attestation.js';
|
|
21
22
|
import { searchCache, LRUCache } from './cache.js';
|
|
23
|
+
import { jaccardSimilarity } from './text-utils.js';
|
|
22
24
|
|
|
23
25
|
let lastDataVersion = 0;
|
|
24
26
|
|
|
@@ -68,7 +70,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
68
70
|
const vecHits = searchVector(queryEmbedding, parsedLimit * 2);
|
|
69
71
|
|
|
70
72
|
const semanticResults = vecHits.map(r => ({
|
|
71
|
-
id: r.rowid,
|
|
73
|
+
id: Number(r.rowid),
|
|
72
74
|
distance: r.distance,
|
|
73
75
|
// Convert L2 distance to 0-1 similarity score
|
|
74
76
|
similarity: Math.max(0, 1 - (r.distance * r.distance) / 2)
|
|
@@ -116,7 +118,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
116
118
|
let reputationWarning = false;
|
|
117
119
|
const prov = memory.provenance;
|
|
118
120
|
if (prov && prov.source_type === 'agent' && prov.source_id) {
|
|
119
|
-
const agentRow =
|
|
121
|
+
const agentRow = stmts.getReputationScore.get(prov.source_id);
|
|
120
122
|
if (agentRow) {
|
|
121
123
|
reputationScore = agentRow.reputation_score;
|
|
122
124
|
if (reputationScore < 0.5) {
|
|
@@ -134,8 +136,8 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
134
136
|
importance_score: memory.importance_score,
|
|
135
137
|
created_at: memory.created_at,
|
|
136
138
|
last_accessed: memory.last_accessed,
|
|
137
|
-
similarity: r.similarity
|
|
138
|
-
hybrid_score:
|
|
139
|
+
similarity: Math.round(r.similarity * 10000) / 10000,
|
|
140
|
+
hybrid_score: Math.round(finalScore * 10000) / 10000,
|
|
139
141
|
keyword_match: r.keyword_match,
|
|
140
142
|
reputation_warning: reputationWarning,
|
|
141
143
|
provenance: prov
|
|
@@ -217,27 +219,6 @@ function applyMMR(candidates, limit, lambda = 0.7) {
|
|
|
217
219
|
return selected;
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
/**
|
|
221
|
-
* Compute Jaccard similarity between two text strings.
|
|
222
|
-
* Uses word-level tokenization for efficiency.
|
|
223
|
-
*
|
|
224
|
-
* @param {string} a - First text
|
|
225
|
-
* @param {string} b - Second text
|
|
226
|
-
* @returns {number} Similarity score between 0 and 1
|
|
227
|
-
*/
|
|
228
|
-
function jaccardSimilarity(a, b) {
|
|
229
|
-
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
230
|
-
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
231
|
-
|
|
232
|
-
let intersection = 0;
|
|
233
|
-
for (const word of wordsA) {
|
|
234
|
-
if (wordsB.has(word)) intersection++;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const union = wordsA.size + wordsB.size - intersection;
|
|
238
|
-
return union === 0 ? 0 : intersection / union;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
222
|
/**
|
|
242
223
|
* Optimizes the retrieved context by walking the knowledge graph and compressing content to fit max_tokens.
|
|
243
224
|
*
|
|
@@ -246,7 +227,14 @@ function jaccardSimilarity(a, b) {
|
|
|
246
227
|
* @param {string|null} agentId - Querying agent identifier
|
|
247
228
|
* @param {string|null} sessionId - Current session ID
|
|
248
229
|
*/
|
|
249
|
-
export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null) {
|
|
230
|
+
export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null, intentParam = null) {
|
|
231
|
+
// Classify intent and urgency early to adjust token budget dynamically
|
|
232
|
+
const { intent, urgency } = classifyIntentAndUrgency(queryText, intentParam);
|
|
233
|
+
let targetMaxTokens = maxTokens;
|
|
234
|
+
if (intent === 'general' || intent === 'testing') {
|
|
235
|
+
targetMaxTokens = Math.min(maxTokens, 1500);
|
|
236
|
+
}
|
|
237
|
+
|
|
250
238
|
// Extract entities mentioned in the query text to seed the graph search directly
|
|
251
239
|
const entities = getAllEntities(100);
|
|
252
240
|
const matchedEntityIds = new Set();
|
|
@@ -302,11 +290,7 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
302
290
|
if (depth >= 6) continue;
|
|
303
291
|
|
|
304
292
|
// --- 2a. Explicit Graph Edges (from edges table) ---
|
|
305
|
-
const connectedEdges =
|
|
306
|
-
SELECT * FROM edges
|
|
307
|
-
WHERE (source_id = ? AND source_type = ?)
|
|
308
|
-
OR (target_id = ? AND target_type = ?)
|
|
309
|
-
`).all(id, type, id, type);
|
|
293
|
+
const connectedEdges = stmts.getEdgesBySourceAndType.all(id, type, id, type);
|
|
310
294
|
|
|
311
295
|
for (const edge of connectedEdges) {
|
|
312
296
|
let nextId, nextType;
|
|
@@ -327,7 +311,7 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
327
311
|
|
|
328
312
|
// --- 2b. Implicit Name-Based Edges (for robustness when explicit edges are missing) ---
|
|
329
313
|
if (type === 'memory') {
|
|
330
|
-
const memoryRow =
|
|
314
|
+
const memoryRow = stmts.getMemoryContentById.get(id);
|
|
331
315
|
if (memoryRow && memoryRow.content) {
|
|
332
316
|
const contentLower = memoryRow.content.toLowerCase();
|
|
333
317
|
for (const ent of entities) {
|
|
@@ -343,7 +327,7 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
343
327
|
} else if (type === 'entity') {
|
|
344
328
|
const ent = entities.find(e => e.id === id);
|
|
345
329
|
if (ent && ent.name) {
|
|
346
|
-
const matchingMemories =
|
|
330
|
+
const matchingMemories = stmts.getMemoryLikeContent.all(`%${ent.name}%`);
|
|
347
331
|
for (const row of matchingMemories) {
|
|
348
332
|
const nextKey = `memory:${row.id}`;
|
|
349
333
|
if (!visitedNodes.has(nextKey)) {
|
|
@@ -409,31 +393,51 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
409
393
|
// 4. Sort candidates
|
|
410
394
|
list.sort((a, b) => b.score - a.score);
|
|
411
395
|
|
|
412
|
-
// 5. Compress context to fit maxTokens
|
|
396
|
+
// 5. Compress context to fit maxTokens with on-the-fly diversity check
|
|
413
397
|
let currentTokens = 0;
|
|
414
398
|
const accepted = [];
|
|
415
399
|
|
|
416
400
|
for (const c of list) {
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
401
|
+
// Skip if too similar to any already accepted memory to prevent redundant context bloat
|
|
402
|
+
let isRedundant = false;
|
|
403
|
+
for (const acc of accepted) {
|
|
404
|
+
const sim = jaccardSimilarity(c.content, acc.content);
|
|
405
|
+
if (sim > 0.60) {
|
|
406
|
+
isRedundant = true;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (isRedundant) continue;
|
|
411
|
+
|
|
412
|
+
// Heuristic: ~4 characters per token + format headers (~3 tokens for compact format)
|
|
413
|
+
const estimatedTokens = Math.max(1, Math.ceil(c.content.length / 4) + 3);
|
|
414
|
+
if (currentTokens + estimatedTokens > targetMaxTokens) {
|
|
420
415
|
continue;
|
|
421
416
|
}
|
|
422
417
|
currentTokens += estimatedTokens;
|
|
423
418
|
accepted.push(c);
|
|
424
419
|
}
|
|
425
420
|
|
|
421
|
+
const suggested_actions = generateSuggestedActions(accepted, intent, urgency);
|
|
422
|
+
|
|
426
423
|
// 6. Format LLM injection context string
|
|
427
424
|
let context = '=== RETRIEVED AGENT MEMORY CONTEXT ===\n';
|
|
425
|
+
context += `[Intent: ${intent} | Urgency: ${urgency}]\n\n`;
|
|
426
|
+
|
|
427
|
+
if (suggested_actions.length > 0) {
|
|
428
|
+
context += '[Suggested Actions]\n';
|
|
429
|
+
for (const action of suggested_actions) {
|
|
430
|
+
context += `• ${action}\n`;
|
|
431
|
+
}
|
|
432
|
+
context += '\n';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
context += '[Memories]\n';
|
|
428
436
|
if (accepted.length === 0) {
|
|
429
437
|
context += 'No relevant memories retrieved.\n';
|
|
430
438
|
} else {
|
|
431
439
|
for (const a of accepted) {
|
|
432
|
-
|
|
433
|
-
if (a.provenance) {
|
|
434
|
-
sourceTag = `Source: ${a.provenance.source_type}${a.provenance.source_id ? ` (${a.provenance.source_id})` : ''}`;
|
|
435
|
-
}
|
|
436
|
-
context += `[Memory #${a.id}] (Score: ${a.score.toFixed(4)}, ${sourceTag})\n${a.content}\n---\n`;
|
|
440
|
+
context += `#${a.id}: ${a.content}\n`;
|
|
437
441
|
}
|
|
438
442
|
}
|
|
439
443
|
context += '=== END OF CONTEXT ===';
|
|
@@ -447,7 +451,10 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
447
451
|
return {
|
|
448
452
|
context,
|
|
449
453
|
memories: accepted,
|
|
450
|
-
attestation
|
|
454
|
+
attestation,
|
|
455
|
+
intent,
|
|
456
|
+
urgency,
|
|
457
|
+
suggested_actions
|
|
451
458
|
};
|
|
452
459
|
}
|
|
453
460
|
|
|
@@ -514,26 +521,22 @@ export async function consolidateMemories(namespace = null) {
|
|
|
514
521
|
const consolidated = [];
|
|
515
522
|
const visited = new Set();
|
|
516
523
|
|
|
517
|
-
|
|
518
|
-
|
|
524
|
+
// Wrap all mutations in a transaction so a partial failure rolls back.
|
|
525
|
+
const consolidateOne = db.transaction((mem) => {
|
|
526
|
+
if (visited.has(mem.id)) return;
|
|
519
527
|
|
|
520
528
|
// Search for similar memories
|
|
521
|
-
const embedding =
|
|
522
|
-
if (!embedding)
|
|
529
|
+
const embedding = stmts.getVecByRowId.get(mem.id);
|
|
530
|
+
if (!embedding) return;
|
|
523
531
|
|
|
524
|
-
const hits =
|
|
525
|
-
SELECT rowid AS id, distance
|
|
526
|
-
FROM memories_vec
|
|
527
|
-
WHERE embedding MATCH ?
|
|
528
|
-
AND k = 30
|
|
529
|
-
`).all(embedding.embedding);
|
|
532
|
+
const hits = stmts.consolidateVecSearch.all(embedding.embedding);
|
|
530
533
|
|
|
531
534
|
const group = [];
|
|
532
535
|
for (const hit of hits) {
|
|
533
536
|
if (visited.has(Number(hit.id))) continue;
|
|
534
537
|
const sim = Math.max(0, 1 - (hit.distance * hit.distance) / 2);
|
|
535
538
|
if (sim > 0.80) {
|
|
536
|
-
const other =
|
|
539
|
+
const other = stmts.getMemoryByIdRaw.get(Number(hit.id));
|
|
537
540
|
if (other) {
|
|
538
541
|
group.push(other);
|
|
539
542
|
}
|
|
@@ -546,7 +549,7 @@ export async function consolidateMemories(namespace = null) {
|
|
|
546
549
|
const prov = getProvenance(m.id);
|
|
547
550
|
let reputation = 1.0;
|
|
548
551
|
if (prov && prov.source_type === 'agent' && prov.source_id) {
|
|
549
|
-
const agentRow =
|
|
552
|
+
const agentRow = stmts.getReputationScore.get(prov.source_id);
|
|
550
553
|
if (agentRow) reputation = agentRow.reputation_score;
|
|
551
554
|
}
|
|
552
555
|
return (prov ? prov.confidence : 1.0) * reputation;
|
|
@@ -565,50 +568,32 @@ export async function consolidateMemories(namespace = null) {
|
|
|
565
568
|
const rel = checkRelationship(canonical.content, current.content);
|
|
566
569
|
|
|
567
570
|
if (rel.type === 'contradiction') {
|
|
568
|
-
// Resolve contradiction: keep canonical, archive current
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// Apply reputation changes since it's a cross-agent contradiction
|
|
574
|
-
const oldProv = getProvenance(current.id);
|
|
575
|
-
const newProv = getProvenance(canonical.id);
|
|
576
|
-
if (oldProv && oldProv.source_type === 'agent' && oldProv.source_id) {
|
|
577
|
-
const isSelf = newProv && newProv.source_type === 'agent' && newProv.source_id === oldProv.source_id;
|
|
578
|
-
if (!isSelf) {
|
|
579
|
-
db.prepare('UPDATE agent_stats SET memories_contradicted = memories_contradicted + 1 WHERE agent_id = ?').run(oldProv.source_id);
|
|
580
|
-
db.prepare('UPDATE agent_stats SET reputation_score = (memories_confirmed + 1.0) / (memories_contradicted + 1.0) WHERE agent_id = ?').run(oldProv.source_id);
|
|
581
|
-
if (newProv && newProv.source_type === 'agent') {
|
|
582
|
-
db.prepare('UPDATE agent_stats SET memories_confirmed = memories_confirmed + 1 WHERE agent_id = ?').run(newProv.source_id);
|
|
583
|
-
db.prepare('UPDATE agent_stats SET reputation_score = (memories_confirmed + 1.0) / (memories_contradicted + 1.0) WHERE agent_id = ?').run(newProv.source_id);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
571
|
+
// Resolve contradiction: keep canonical, archive current.
|
|
572
|
+
// logContradiction already updates agent stats, so we only record the archive here.
|
|
573
|
+
stmts.archiveMemoryById.run(current.id);
|
|
574
|
+
stmts.insertContradiction.run(current.id, canonical.id, `Consolidated contradiction: resolved in favor of canonical #${canonical.id}`);
|
|
587
575
|
|
|
588
576
|
archivedIds.push(current.id);
|
|
589
577
|
visited.add(current.id);
|
|
590
578
|
} else if (rel.type === 'subset') {
|
|
591
579
|
if (rel.keep === 'b') {
|
|
592
580
|
// current (B) is a superset of canonical (A). Swap them
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
.run(canonical.id, current.id, `Consolidated subset: replaced by more detailed #${current.id}`);
|
|
581
|
+
stmts.archiveMemoryById.run(canonical.id);
|
|
582
|
+
stmts.insertContradiction.run(canonical.id, current.id, `Consolidated subset: replaced by more detailed #${current.id}`);
|
|
596
583
|
|
|
597
584
|
archivedIds.push(canonical.id);
|
|
598
585
|
canonical = current;
|
|
599
586
|
} else {
|
|
600
587
|
// canonical is superset
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
.run(current.id, canonical.id, `Consolidated subset: subsumed by more detailed #${canonical.id}`);
|
|
588
|
+
stmts.archiveMemoryById.run(current.id);
|
|
589
|
+
stmts.insertContradiction.run(current.id, canonical.id, `Consolidated subset: subsumed by more detailed #${canonical.id}`);
|
|
604
590
|
|
|
605
591
|
archivedIds.push(current.id);
|
|
606
592
|
}
|
|
607
593
|
visited.add(current.id);
|
|
608
594
|
} else if (rel.type === 'duplicate') {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
.run(current.id, canonical.id, `Consolidated duplicate of #${canonical.id}`);
|
|
595
|
+
stmts.archiveMemoryById.run(current.id);
|
|
596
|
+
stmts.insertContradiction.run(current.id, canonical.id, `Consolidated duplicate of #${canonical.id}`);
|
|
612
597
|
|
|
613
598
|
archivedIds.push(current.id);
|
|
614
599
|
visited.add(current.id);
|
|
@@ -623,6 +608,10 @@ export async function consolidateMemories(namespace = null) {
|
|
|
623
608
|
});
|
|
624
609
|
}
|
|
625
610
|
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
for (const mem of activeMemories) {
|
|
614
|
+
consolidateOne(mem);
|
|
626
615
|
}
|
|
627
616
|
|
|
628
617
|
return {
|
|
@@ -631,3 +620,75 @@ export async function consolidateMemories(namespace = null) {
|
|
|
631
620
|
details: consolidated
|
|
632
621
|
};
|
|
633
622
|
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Classify context retrieval intent and urgency level using heuristic analysis.
|
|
626
|
+
*/
|
|
627
|
+
function classifyIntentAndUrgency(queryText, intentParam = null) {
|
|
628
|
+
const queryLower = (queryText || '').toLowerCase();
|
|
629
|
+
|
|
630
|
+
// 1. Determine Intent
|
|
631
|
+
let intent = intentParam || 'general';
|
|
632
|
+
if (intent === 'general' || !intent) {
|
|
633
|
+
if (/(?:db|database|sqlite|sql|table|migration|schema)/i.test(queryLower)) {
|
|
634
|
+
intent = 'database_management';
|
|
635
|
+
} else if (/(?:deploy|ci|cd|vercel|publish|release|prod|staging)/i.test(queryLower)) {
|
|
636
|
+
intent = 'deployment';
|
|
637
|
+
} else if (/(?:style|css|html|theme|design|layout|align|color|font)/i.test(queryLower)) {
|
|
638
|
+
intent = 'ui_styling';
|
|
639
|
+
} else if (/(?:test|spec|unit|mock|heavy|smoke)/i.test(queryLower)) {
|
|
640
|
+
intent = 'testing';
|
|
641
|
+
} else if (/(?:error|bug|fail|crash|break|exception|stack|trace|refused|debug)/i.test(queryLower)) {
|
|
642
|
+
intent = 'debugging';
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// 2. Determine Urgency
|
|
647
|
+
let urgency = 'low';
|
|
648
|
+
if (/(?:panic|emergency|broken|critical|urgent|fatal|security|leak|bypass|vulnerability)/i.test(queryLower)) {
|
|
649
|
+
urgency = 'critical';
|
|
650
|
+
} else if (/(?:fail|error|crash|prevent|stop|warn|warning|issue|broken)/i.test(queryLower)) {
|
|
651
|
+
urgency = 'high';
|
|
652
|
+
} else if (/(?:update|change|add|tweak|check|verify)/i.test(queryLower)) {
|
|
653
|
+
urgency = 'medium';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return { intent, urgency };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Generate actionable suggested actions based on active memories and query classification.
|
|
661
|
+
*/
|
|
662
|
+
function generateSuggestedActions(memories, intent, urgency) {
|
|
663
|
+
const actions = [];
|
|
664
|
+
|
|
665
|
+
// General recommendation based on intent
|
|
666
|
+
if (intent === 'debugging') {
|
|
667
|
+
actions.push('Inspect the recent error logs and verify SQLite/system constraints.');
|
|
668
|
+
} else if (intent === 'ui_styling') {
|
|
669
|
+
actions.push('Verify UI layouts conform to user design preferences.');
|
|
670
|
+
} else if (intent === 'database_management') {
|
|
671
|
+
actions.push('Ensure database migrations are applied and referential integrity is checked.');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
for (const m of memories) {
|
|
675
|
+
const content = m.content.toLowerCase();
|
|
676
|
+
|
|
677
|
+
// Check for rules/decisions in memory content
|
|
678
|
+
if (content.includes('decision:') || content.includes('rule:')) {
|
|
679
|
+
actions.push(`Adhere to guideline: ${m.content.slice(0, 100)}...`);
|
|
680
|
+
} else if (content.includes('prefer')) {
|
|
681
|
+
actions.push(`Apply user preference: ${m.content.slice(0, 100)}...`);
|
|
682
|
+
} else if (content.includes('error') || content.includes('bug') || content.includes('fix')) {
|
|
683
|
+
actions.push(`Reference past fix: ${m.content.slice(0, 100)}...`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Safety guideline if critical
|
|
688
|
+
if (urgency === 'critical') {
|
|
689
|
+
actions.unshift('CAUTION: Address security, vulnerability, or critical stability factors immediately.');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Deduplicate
|
|
693
|
+
return Array.from(new Set(actions));
|
|
694
|
+
}
|