persyst-mcp 2.2.0 → 2.2.2
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/bin/extract-worker.js +330 -14
- package/bin/setup.js +6 -0
- package/hooks/persyst-hook.js +72 -9
- package/package.json +2 -2
- package/src/attestation.js +7 -1
- package/src/database.js +81 -21
- package/src/search.js +127 -61
- package/src/server.js +116 -13
- package/src/tools.js +187 -114
package/src/database.js
CHANGED
|
@@ -77,6 +77,11 @@ try {
|
|
|
77
77
|
db.exec("ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'shared'");
|
|
78
78
|
} catch (e) { /* Column already exists */ }
|
|
79
79
|
|
|
80
|
+
// --- Migration: add parent_id column for history tracing ---
|
|
81
|
+
try {
|
|
82
|
+
db.exec('ALTER TABLE memories ADD COLUMN parent_id INTEGER DEFAULT NULL');
|
|
83
|
+
} catch (e) { /* Column already exists */ }
|
|
84
|
+
|
|
80
85
|
// --- Index on namespace for fast filtered queries ---
|
|
81
86
|
try {
|
|
82
87
|
db.exec('CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories (namespace)');
|
|
@@ -226,7 +231,7 @@ console.error('[persyst] Schema initialized ✓');
|
|
|
226
231
|
const stmts = {
|
|
227
232
|
// -- Insert --
|
|
228
233
|
insertMemory: db.prepare(
|
|
229
|
-
'INSERT INTO memories (content, importance_score, namespace) VALUES (?, ?, ?)'
|
|
234
|
+
'INSERT INTO memories (content, importance_score, namespace, parent_id) VALUES (?, ?, ?, ?)'
|
|
230
235
|
),
|
|
231
236
|
insertVec: db.prepare(
|
|
232
237
|
'INSERT INTO memories_vec (rowid, embedding) VALUES (?, ?)'
|
|
@@ -283,7 +288,7 @@ const stmts = {
|
|
|
283
288
|
"SELECT * FROM memories WHERE (namespace = ? OR namespace = 'shared') AND valid_until IS NULL ORDER BY importance_score DESC LIMIT ?"
|
|
284
289
|
),
|
|
285
290
|
getProvenance: db.prepare(
|
|
286
|
-
'SELECT * FROM provenance WHERE memory_id = ?'
|
|
291
|
+
'SELECT * FROM provenance WHERE memory_id = ? ORDER BY id DESC'
|
|
287
292
|
),
|
|
288
293
|
getAllAgentStats: db.prepare(
|
|
289
294
|
'SELECT * FROM agent_stats ORDER BY reputation_score DESC'
|
|
@@ -434,13 +439,19 @@ const stmts = {
|
|
|
434
439
|
* @param {string} namespace - Namespace for agent isolation (default: 'shared')
|
|
435
440
|
* @returns {number} The new memory's ID
|
|
436
441
|
*/
|
|
437
|
-
export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared') {
|
|
438
|
-
|
|
442
|
+
export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared', parentId = null) {
|
|
443
|
+
if (content && content.length > 10000) {
|
|
444
|
+
throw new Error('Memory content exceeds maximum length of 10000 characters.');
|
|
445
|
+
}
|
|
446
|
+
const result = stmts.insertMemory.run(content, importance, namespace || 'shared', parentId);
|
|
439
447
|
const id = Number(result.lastInsertRowid);
|
|
440
448
|
|
|
441
449
|
// Provenance Info handling
|
|
442
450
|
const source_type = provenanceInfo?.source_type || 'manual';
|
|
443
|
-
|
|
451
|
+
let source_id = provenanceInfo?.source_id || null;
|
|
452
|
+
if (source_type === 'agent' && source_id) {
|
|
453
|
+
source_id = source_id.toLowerCase();
|
|
454
|
+
}
|
|
444
455
|
const confidence = provenanceInfo?.confidence !== undefined ? provenanceInfo.confidence : 1.0;
|
|
445
456
|
|
|
446
457
|
stmts.insertProvenance.run(id, source_type, source_id, confidence);
|
|
@@ -533,6 +544,12 @@ export function deleteVec(id) {
|
|
|
533
544
|
export function deleteMemory(id) {
|
|
534
545
|
stmts.deleteEdgesByMemory.run(id, id);
|
|
535
546
|
deleteVec(id); // Remove vector first (no cascades on virtual tables)
|
|
547
|
+
try {
|
|
548
|
+
db.prepare('DELETE FROM provenance WHERE memory_id = ?').run(id);
|
|
549
|
+
db.prepare('DELETE FROM contradictions WHERE old_memory_id = ? OR new_memory_id = ?').run(id, id);
|
|
550
|
+
} catch (e) {
|
|
551
|
+
console.error(`[persyst] Clean up provenance/contradictions error: ${e.message}`);
|
|
552
|
+
}
|
|
536
553
|
const result = stmts.deleteMemory.run(id);
|
|
537
554
|
return result.changes > 0;
|
|
538
555
|
}
|
|
@@ -758,14 +775,23 @@ export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
|
758
775
|
stmts.archiveMemory.run(oldMemoryId);
|
|
759
776
|
stmts.insertContradiction.run(oldMemoryId, newMemoryId, reason);
|
|
760
777
|
|
|
778
|
+
// Set parent_id to link memories for bidirectional history tracing (always newer pointing to older)
|
|
779
|
+
try {
|
|
780
|
+
const parentId = Math.min(oldMemoryId, newMemoryId);
|
|
781
|
+
const childId = Math.max(oldMemoryId, newMemoryId);
|
|
782
|
+
db.prepare('UPDATE memories SET parent_id = ? WHERE id = ?').run(parentId, childId);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
console.error(`[persyst] Failed to set parent_id on contradiction: ${e.message}`);
|
|
785
|
+
}
|
|
786
|
+
|
|
761
787
|
// Retrieve provenance of both versions for game-theoretic reputation calculation
|
|
762
788
|
const oldProvenance = getProvenance(oldMemoryId);
|
|
763
789
|
const newProvenance = getProvenance(newMemoryId);
|
|
764
790
|
|
|
765
791
|
if (oldProvenance && oldProvenance.source_type === 'agent' && oldProvenance.source_id) {
|
|
766
|
-
const isSelfCorrection = newProvenance &&
|
|
767
|
-
|
|
768
|
-
|
|
792
|
+
const isSelfCorrection = (newProvenance && newProvenance.source_id &&
|
|
793
|
+
newProvenance.source_id.toLowerCase() === oldProvenance.source_id.toLowerCase()) ||
|
|
794
|
+
reason.includes('update_memory');
|
|
769
795
|
if (!isSelfCorrection) {
|
|
770
796
|
// Different agent/manual source contradicts the old memory
|
|
771
797
|
incrementAgentStat(oldProvenance.source_id, 'contradicted');
|
|
@@ -782,22 +808,30 @@ export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
|
782
808
|
* Get provenance for a memory.
|
|
783
809
|
*/
|
|
784
810
|
export function getProvenance(memoryId) {
|
|
785
|
-
|
|
811
|
+
const prov = stmts.getProvenance.get(memoryId) || null;
|
|
812
|
+
if (prov && prov.source_type === 'agent' && prov.source_id) {
|
|
813
|
+
prov.source_id = prov.source_id.toLowerCase();
|
|
814
|
+
}
|
|
815
|
+
return prov;
|
|
786
816
|
}
|
|
787
817
|
|
|
788
818
|
/**
|
|
789
819
|
* Update agent reputation counters.
|
|
790
820
|
*/
|
|
791
821
|
export function incrementAgentStat(agentId, action) {
|
|
792
|
-
|
|
822
|
+
const normalizedAgentId = agentId.toLowerCase();
|
|
823
|
+
if (normalizedAgentId === 'antigravity-worker' || normalizedAgentId === 'user-dialogue') {
|
|
824
|
+
return; // Ignore internal/system identities from reputation penalties
|
|
825
|
+
}
|
|
826
|
+
stmts.upsertAgent.run(normalizedAgentId);
|
|
793
827
|
if (action === 'created') {
|
|
794
|
-
stmts.incrementCreated.run(
|
|
828
|
+
stmts.incrementCreated.run(normalizedAgentId);
|
|
795
829
|
} else if (action === 'confirmed') {
|
|
796
|
-
stmts.incrementConfirmed.run(
|
|
830
|
+
stmts.incrementConfirmed.run(normalizedAgentId);
|
|
797
831
|
} else if (action === 'contradicted') {
|
|
798
|
-
stmts.incrementContradicted.run(
|
|
832
|
+
stmts.incrementContradicted.run(normalizedAgentId);
|
|
799
833
|
}
|
|
800
|
-
stmts.recalculateReputation.run(
|
|
834
|
+
stmts.recalculateReputation.run(normalizedAgentId);
|
|
801
835
|
}
|
|
802
836
|
|
|
803
837
|
/**
|
|
@@ -857,13 +891,25 @@ export function getMemoryHistoryChain(memoryId) {
|
|
|
857
891
|
if (versions.has(currentId)) continue;
|
|
858
892
|
versions.add(currentId);
|
|
859
893
|
|
|
860
|
-
// Find
|
|
894
|
+
// 1. Find parent (ancestor) from memories table
|
|
895
|
+
const row = db.prepare('SELECT parent_id FROM memories WHERE id = ?').get(currentId);
|
|
896
|
+
if (row && row.parent_id !== null) {
|
|
897
|
+
if (!versions.has(row.parent_id)) queue.push(row.parent_id);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 2. Find children (descendants) from memories table
|
|
901
|
+
const children = db.prepare('SELECT id FROM memories WHERE parent_id = ?').all(currentId);
|
|
902
|
+
for (const child of children) {
|
|
903
|
+
if (!versions.has(child.id)) queue.push(child.id);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// 3. Fallback: Find ancestors (replaced by current) from contradictions table
|
|
861
907
|
const ancestors = stmts.getContradictionAncestors.all(currentId);
|
|
862
908
|
ancestors.forEach(a => {
|
|
863
909
|
if (!versions.has(a.old_memory_id)) queue.push(a.old_memory_id);
|
|
864
910
|
});
|
|
865
911
|
|
|
866
|
-
// Find descendants (replaces current)
|
|
912
|
+
// 4. Fallback: Find descendants (replaces current) from contradictions table
|
|
867
913
|
const descendants = stmts.getContradictionDescendants.all(currentId);
|
|
868
914
|
descendants.forEach(d => {
|
|
869
915
|
if (!versions.has(d.new_memory_id)) queue.push(d.new_memory_id);
|
|
@@ -875,13 +921,27 @@ export function getMemoryHistoryChain(memoryId) {
|
|
|
875
921
|
|
|
876
922
|
const placeholders = ids.map(() => '?').join(',');
|
|
877
923
|
const rows = db.prepare(`
|
|
878
|
-
SELECT
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
WHERE m.id IN (${placeholders})
|
|
882
|
-
ORDER BY m.created_at ASC
|
|
924
|
+
SELECT * FROM memories
|
|
925
|
+
WHERE id IN (${placeholders})
|
|
926
|
+
ORDER BY created_at ASC
|
|
883
927
|
`).all(...ids);
|
|
884
928
|
|
|
929
|
+
for (const row of rows) {
|
|
930
|
+
const prov = getProvenance(row.id);
|
|
931
|
+
if (prov) {
|
|
932
|
+
row.source_type = prov.source_type;
|
|
933
|
+
row.source_id = prov.source_id;
|
|
934
|
+
row.confidence = prov.confidence;
|
|
935
|
+
} else {
|
|
936
|
+
row.source_type = 'manual';
|
|
937
|
+
row.source_id = null;
|
|
938
|
+
row.confidence = 1.0;
|
|
939
|
+
}
|
|
940
|
+
if (row.source_type === 'agent' && row.source_id) {
|
|
941
|
+
row.source_id = row.source_id.toLowerCase();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
885
945
|
return rows;
|
|
886
946
|
}
|
|
887
947
|
|
package/src/search.js
CHANGED
|
@@ -13,7 +13,8 @@ import db, {
|
|
|
13
13
|
getMemoryById,
|
|
14
14
|
boostMemory,
|
|
15
15
|
getProvenance,
|
|
16
|
-
getMemoriesByEntity
|
|
16
|
+
getMemoriesByEntity,
|
|
17
|
+
getAllEntities
|
|
17
18
|
} from './database.js';
|
|
18
19
|
import { generateEmbedding } from './embeddings.js';
|
|
19
20
|
import { createAttestation } from './attestation.js';
|
|
@@ -31,7 +32,7 @@ let lastDataVersion = 0;
|
|
|
31
32
|
* @param {string|null} sessionId - Session identifier
|
|
32
33
|
* @returns {Promise<Array>} Ranked search results (with .attestation property attached)
|
|
33
34
|
*/
|
|
34
|
-
export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null) {
|
|
35
|
+
export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null, skipAttestation = false) {
|
|
35
36
|
// Sync in-memory cache with external DB changes using sqlite data_version
|
|
36
37
|
try {
|
|
37
38
|
const currentDataVersion = db.pragma('data_version', { simple: true });
|
|
@@ -142,11 +143,12 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
142
143
|
// --- Step 5: Apply MMR for diverse retrieval (Feature 3) ---
|
|
143
144
|
const mmrResults = applyMMR(finalResults, limit);
|
|
144
145
|
|
|
145
|
-
// Generate cryptographic attestation for audit trails
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
// Generate cryptographic attestation for audit trails (skip if called internally)
|
|
147
|
+
let attestation = null;
|
|
148
|
+
if (!skipAttestation) {
|
|
149
|
+
attestation = createAttestation(queryText, mmrResults, agentId, sessionId);
|
|
150
|
+
mmrResults.attestation = attestation;
|
|
151
|
+
}
|
|
150
152
|
|
|
151
153
|
// --- Store in LRU cache (Feature 1) ---
|
|
152
154
|
searchCache.set(cacheKey, mmrResults);
|
|
@@ -239,8 +241,18 @@ function jaccardSimilarity(a, b) {
|
|
|
239
241
|
* @param {string|null} sessionId - Current session ID
|
|
240
242
|
*/
|
|
241
243
|
export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null) {
|
|
242
|
-
//
|
|
243
|
-
const
|
|
244
|
+
// Extract entities mentioned in the query text to seed the graph search directly
|
|
245
|
+
const entities = getAllEntities(100);
|
|
246
|
+
const matchedEntityIds = new Set();
|
|
247
|
+
for (const ent of entities) {
|
|
248
|
+
const entNameLower = ent.name.toLowerCase();
|
|
249
|
+
if (queryText.toLowerCase().includes(entNameLower)) {
|
|
250
|
+
matchedEntityIds.add(ent.id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 1. Run hybrid search to fetch top 5 memories as seeds (skip attestation to avoid double-write)
|
|
255
|
+
const searchHits = await searchHybrid(queryText, 5, agentId, sessionId, namespace, true);
|
|
244
256
|
const candidates = new Map();
|
|
245
257
|
|
|
246
258
|
for (const hit of searchHits) {
|
|
@@ -254,63 +266,117 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
|
|
|
254
266
|
provenance: hit.provenance,
|
|
255
267
|
source: 'search'
|
|
256
268
|
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2. Perform Graph Hop (multi-hop traversal) globally
|
|
272
|
+
const hopQueue = [];
|
|
273
|
+
const visitedNodes = new Set(); // Stores "type:id" keys
|
|
274
|
+
|
|
275
|
+
// Seed with matched entities from query text
|
|
276
|
+
for (const entId of matchedEntityIds) {
|
|
277
|
+
const key = `entity:${entId}`;
|
|
278
|
+
if (!visitedNodes.has(key)) {
|
|
279
|
+
visitedNodes.add(key);
|
|
280
|
+
hopQueue.push({ id: entId, type: 'entity', depth: 0 });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Seed with search hit memories
|
|
285
|
+
for (const hit of searchHits) {
|
|
286
|
+
const key = `memory:${hit.id}`;
|
|
287
|
+
if (!visitedNodes.has(key)) {
|
|
288
|
+
visitedNodes.add(key);
|
|
289
|
+
hopQueue.push({ id: hit.id, type: 'memory', depth: 0 });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// BFS to traverse memories and entities uniformly up to depth 6
|
|
294
|
+
while (hopQueue.length > 0) {
|
|
295
|
+
const { id, type, depth } = hopQueue.shift();
|
|
296
|
+
if (depth >= 6) continue;
|
|
257
297
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
const hitEdges = db.prepare(`
|
|
298
|
+
// --- 2a. Explicit Graph Edges (from edges table) ---
|
|
299
|
+
const connectedEdges = db.prepare(`
|
|
261
300
|
SELECT * FROM edges
|
|
262
|
-
WHERE (source_id = ? AND source_type =
|
|
263
|
-
OR (target_id = ? AND target_type =
|
|
264
|
-
`).all(
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
if (edge.
|
|
269
|
-
|
|
301
|
+
WHERE (source_id = ? AND source_type = ?)
|
|
302
|
+
OR (target_id = ? AND target_type = ?)
|
|
303
|
+
`).all(id, type, id, type);
|
|
304
|
+
|
|
305
|
+
for (const edge of connectedEdges) {
|
|
306
|
+
let nextId, nextType;
|
|
307
|
+
if (edge.source_id === id && edge.source_type === type) {
|
|
308
|
+
nextId = edge.target_id;
|
|
309
|
+
nextType = edge.target_type;
|
|
310
|
+
} else {
|
|
311
|
+
nextId = edge.source_id;
|
|
312
|
+
nextType = edge.source_type;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const key = `${nextType}:${nextId}`;
|
|
316
|
+
if (!visitedNodes.has(key)) {
|
|
317
|
+
visitedNodes.add(key);
|
|
318
|
+
hopQueue.push({ id: nextId, type: nextType, depth: depth + 1 });
|
|
319
|
+
}
|
|
270
320
|
}
|
|
271
321
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
// --- 2b. Implicit Name-Based Edges (for robustness when explicit edges are missing) ---
|
|
323
|
+
if (type === 'memory') {
|
|
324
|
+
const memoryRow = db.prepare('SELECT content FROM memories WHERE id = ?').get(id);
|
|
325
|
+
if (memoryRow && memoryRow.content) {
|
|
326
|
+
const contentLower = memoryRow.content.toLowerCase();
|
|
327
|
+
for (const ent of entities) {
|
|
328
|
+
if (contentLower.includes(ent.name.toLowerCase())) {
|
|
329
|
+
const nextKey = `entity:${ent.id}`;
|
|
330
|
+
if (!visitedNodes.has(nextKey)) {
|
|
331
|
+
visitedNodes.add(nextKey);
|
|
332
|
+
hopQueue.push({ id: ent.id, type: 'entity', depth: depth + 1 });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else if (type === 'entity') {
|
|
338
|
+
const ent = entities.find(e => e.id === id);
|
|
339
|
+
if (ent && ent.name) {
|
|
340
|
+
const matchingMemories = db.prepare('SELECT id FROM memories WHERE content LIKE ? AND valid_until IS NULL').all(`%${ent.name}%`);
|
|
341
|
+
for (const row of matchingMemories) {
|
|
342
|
+
const nextKey = `memory:${row.id}`;
|
|
343
|
+
if (!visitedNodes.has(nextKey)) {
|
|
344
|
+
visitedNodes.add(nextKey);
|
|
345
|
+
hopQueue.push({ id: row.id, type: 'memory', depth: depth + 1 });
|
|
346
|
+
}
|
|
291
347
|
}
|
|
292
348
|
}
|
|
293
349
|
}
|
|
350
|
+
}
|
|
294
351
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
provenance: otherProv,
|
|
311
|
-
source: 'hop'
|
|
312
|
-
});
|
|
352
|
+
// Now collect all hopped memories from the visited nodes
|
|
353
|
+
for (const key of visitedNodes) {
|
|
354
|
+
const [type, idStr] = key.split(':');
|
|
355
|
+
if (type === 'memory') {
|
|
356
|
+
const memId = Number(idStr);
|
|
357
|
+
if (candidates.has(memId)) continue; // Keep search hit info
|
|
358
|
+
|
|
359
|
+
// Check namespace filter if present
|
|
360
|
+
const other = getMemoryById(memId, namespace);
|
|
361
|
+
if (!other) continue;
|
|
362
|
+
|
|
363
|
+
let baseScore = 0.4;
|
|
364
|
+
if (searchHits.length > 0) {
|
|
365
|
+
const maxSearchScore = Math.max(...searchHits.map(h => parseFloat(h.hybrid_score)));
|
|
366
|
+
baseScore = maxSearchScore * 0.5;
|
|
313
367
|
}
|
|
368
|
+
|
|
369
|
+
const otherProv = getProvenance(memId);
|
|
370
|
+
candidates.set(memId, {
|
|
371
|
+
id: other.id,
|
|
372
|
+
content: other.content,
|
|
373
|
+
importance_score: other.importance_score,
|
|
374
|
+
created_at: other.created_at,
|
|
375
|
+
last_accessed: other.last_accessed,
|
|
376
|
+
score: baseScore,
|
|
377
|
+
provenance: otherProv,
|
|
378
|
+
source: 'hop'
|
|
379
|
+
});
|
|
314
380
|
}
|
|
315
381
|
}
|
|
316
382
|
|
|
@@ -408,7 +474,7 @@ function checkRelationship(a, b) {
|
|
|
408
474
|
}
|
|
409
475
|
|
|
410
476
|
// Contradiction: similar topic, differing key terms
|
|
411
|
-
if (jaccard > 0.15 && jaccard < 0.
|
|
477
|
+
if (jaccard > 0.15 && jaccard < 0.65) {
|
|
412
478
|
return { type: 'contradiction' };
|
|
413
479
|
}
|
|
414
480
|
|
|
@@ -453,14 +519,14 @@ export async function consolidateMemories(namespace = null) {
|
|
|
453
519
|
SELECT rowid AS id, distance
|
|
454
520
|
FROM memories_vec
|
|
455
521
|
WHERE embedding MATCH ?
|
|
456
|
-
AND k =
|
|
522
|
+
AND k = 30
|
|
457
523
|
`).all(embedding.embedding);
|
|
458
524
|
|
|
459
525
|
const group = [];
|
|
460
526
|
for (const hit of hits) {
|
|
461
527
|
if (visited.has(Number(hit.id))) continue;
|
|
462
528
|
const sim = Math.max(0, 1 - (hit.distance * hit.distance) / 2);
|
|
463
|
-
if (sim > 0.
|
|
529
|
+
if (sim > 0.80) {
|
|
464
530
|
const other = db.prepare('SELECT * FROM memories WHERE id = ? AND valid_until IS NULL').get(Number(hit.id));
|
|
465
531
|
if (other) {
|
|
466
532
|
group.push(other);
|
|
@@ -481,7 +547,7 @@ export async function consolidateMemories(namespace = null) {
|
|
|
481
547
|
};
|
|
482
548
|
|
|
483
549
|
const groupWithTrust = group.map(m => ({ ...m, trust: getTrust(m) }));
|
|
484
|
-
groupWithTrust.sort((a, b) => b.trust - a.trust || b.importance_score - a.importance_score ||
|
|
550
|
+
groupWithTrust.sort((a, b) => b.trust - a.trust || b.importance_score - a.importance_score || a.id - b.id);
|
|
485
551
|
|
|
486
552
|
// Resolve the group sequentially
|
|
487
553
|
let canonical = groupWithTrust[0];
|
package/src/server.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* server.js — MCP Server Setup
|
|
2
|
+
* server.js — MCP Server & Local HTTP Gateway Setup
|
|
3
3
|
*
|
|
4
|
-
* Creates the MCP server, registers all tools, and connects
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Creates the MCP server, registers all tools, and connects via stdio.
|
|
5
|
+
* Also spins up a local HTTP/JSON Gateway on port 4321 to support low-latency
|
|
6
|
+
* prompt hooks and local agent swarms without subprocess overhead.
|
|
7
7
|
*
|
|
8
|
-
* IMPORTANT: Never write to stdout — it's reserved for MCP protocol.
|
|
9
8
|
* All logging goes to stderr via console.error().
|
|
10
9
|
*/
|
|
11
10
|
|
|
11
|
+
import http from 'http';
|
|
12
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
-
import { registerTools, cleanupWatchers } from './tools.js';
|
|
14
|
+
import { registerTools, cleanupWatchers, addMemoryInternal, executeToolInternal } from './tools.js';
|
|
15
15
|
import { applyTemporalDecay, closeDatabase } from './database.js';
|
|
16
|
-
import { consolidateMemories } from './search.js';
|
|
16
|
+
import { consolidateMemories, searchHybrid, getOptimizedContext } from './search.js';
|
|
17
17
|
import { startWatcher, stopWatcher } from './watcher.js';
|
|
18
|
+
import { verifyChainIntegrity } from './attestation.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Start the Persyst MCP server.
|
|
21
|
-
* This is called from index.js (the entry point).
|
|
21
|
+
* Start the Persyst MCP server & HTTP Gateway.
|
|
22
22
|
*/
|
|
23
23
|
export async function startServer() {
|
|
24
24
|
// --- Create MCP server ---
|
|
25
25
|
const server = new McpServer({
|
|
26
26
|
name: 'persyst',
|
|
27
|
-
version: '2.1
|
|
27
|
+
version: '2.2.1'
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// --- Register all tools ---
|
|
@@ -34,12 +34,114 @@ export async function startServer() {
|
|
|
34
34
|
// --- Start background log watcher daemon ---
|
|
35
35
|
startWatcher();
|
|
36
36
|
|
|
37
|
+
// --- Start local HTTP Gateway (port 4321) ---
|
|
38
|
+
const httpPort = 4321;
|
|
39
|
+
const httpServer = http.createServer((req, res) => {
|
|
40
|
+
// CORS headers for local swarms and browser testing
|
|
41
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
42
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
43
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
44
|
+
|
|
45
|
+
if (req.method === 'OPTIONS') {
|
|
46
|
+
res.writeHead(204);
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (req.method !== 'POST') {
|
|
52
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
53
|
+
res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let body = '';
|
|
58
|
+
req.on('data', chunk => { body += chunk; });
|
|
59
|
+
req.on('end', async () => {
|
|
60
|
+
try {
|
|
61
|
+
const payload = JSON.parse(body || '{}');
|
|
62
|
+
|
|
63
|
+
if (req.url === '/search') {
|
|
64
|
+
const { query, limit = 5, agent_id, session_id } = payload;
|
|
65
|
+
if (!query) {
|
|
66
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
67
|
+
res.end(JSON.stringify({ error: 'Missing required field: query' }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const results = await searchHybrid(query, limit, agent_id, session_id, agent_id || null);
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
72
|
+
res.end(JSON.stringify({ success: true, results }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (req.url === '/add') {
|
|
77
|
+
const { content, importance = 1.0, agent_id, session_id, shared = true } = payload;
|
|
78
|
+
if (!content) {
|
|
79
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
80
|
+
res.end(JSON.stringify({ error: 'Missing required field: content' }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const result = await addMemoryInternal({ content, importance, agent_id, session_id, shared });
|
|
84
|
+
if (result.error) {
|
|
85
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
86
|
+
} else {
|
|
87
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
88
|
+
}
|
|
89
|
+
res.end(JSON.stringify(result));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (req.url === '/context') {
|
|
94
|
+
const { query, max_tokens = 2000, agent_id, session_id } = payload;
|
|
95
|
+
if (!query) {
|
|
96
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ error: 'Missing required field: query' }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const context = await getOptimizedContext(query, max_tokens, agent_id, session_id);
|
|
101
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify(context));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (req.url === '/tool') {
|
|
107
|
+
const { name, arguments: args } = payload;
|
|
108
|
+
if (!name) {
|
|
109
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
110
|
+
res.end(JSON.stringify({ error: 'Missing required field: name' }));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const result = await executeToolInternal(name, args || {});
|
|
114
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
115
|
+
res.end(JSON.stringify(result));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (req.url === '/verify') {
|
|
120
|
+
const result = await verifyChainIntegrity();
|
|
121
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify(result));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'Endpoint Not Found' }));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
130
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
httpServer.listen(httpPort, '127.0.0.1', () => {
|
|
136
|
+
console.error(`[persyst] HTTP Gateway listening on http://127.0.0.1:${httpPort} ✓`);
|
|
137
|
+
});
|
|
138
|
+
|
|
37
139
|
// --- Start temporal decay timer ---
|
|
38
140
|
// Runs every hour: reduces importance of memories not accessed in 7+ days
|
|
39
141
|
const decayTimer = setInterval(applyTemporalDecay, 3600000);
|
|
40
142
|
|
|
41
143
|
// --- Start daily consolidation sweep ---
|
|
42
|
-
// Runs every 24 hours: merges similar memories
|
|
144
|
+
// Runs every 24 hours: merges similar memories
|
|
43
145
|
const consolidationTimer = setInterval(async () => {
|
|
44
146
|
console.error('[persyst] Running scheduled daily memory consolidation sweep...');
|
|
45
147
|
try {
|
|
@@ -50,13 +152,14 @@ export async function startServer() {
|
|
|
50
152
|
}
|
|
51
153
|
}, 86400000);
|
|
52
154
|
|
|
53
|
-
// --- Graceful shutdown
|
|
155
|
+
// --- Graceful shutdown ---
|
|
54
156
|
const shutdown = () => {
|
|
55
157
|
console.error('[persyst] Shutting down...');
|
|
56
158
|
clearInterval(decayTimer);
|
|
57
159
|
clearInterval(consolidationTimer);
|
|
58
160
|
stopWatcher(); // Stop background log watcher
|
|
59
|
-
cleanupWatchers(); //
|
|
161
|
+
cleanupWatchers(); // Stop all git repo watchers
|
|
162
|
+
httpServer.close(); // Close HTTP gateway
|
|
60
163
|
closeDatabase();
|
|
61
164
|
process.exit(0);
|
|
62
165
|
};
|