persyst-mcp 2.2.3 → 2.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "persyst-mcp",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Local-first MCP memory server with hybrid keyword + semantic search for coding agents",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/database.js CHANGED
@@ -323,13 +323,13 @@ const stmts = {
323
323
  boost: db.prepare(`
324
324
  UPDATE memories
325
325
  SET access_count = access_count + 1,
326
- importance_score = MIN(importance_score + 0.1, 2.0),
326
+ importance_score = ROUND(MIN(importance_score + 0.1, 1.0), 4),
327
327
  last_accessed = unixepoch()
328
328
  WHERE id = ?
329
329
  `),
330
330
  decay: db.prepare(`
331
331
  UPDATE memories
332
- SET importance_score = importance_score * 0.95
332
+ SET importance_score = ROUND(MAX(importance_score * 0.95, 0.0), 4)
333
333
  WHERE (? - last_accessed) > 604800
334
334
  `),
335
335
 
@@ -443,7 +443,8 @@ export function insertMemory(content, importance = 1.0, provenanceInfo = null, n
443
443
  if (content && content.length > 10000) {
444
444
  throw new Error('Memory content exceeds maximum length of 10000 characters.');
445
445
  }
446
- const result = stmts.insertMemory.run(content, importance, namespace || 'shared', parentId);
446
+ const clampedImportance = Math.max(0.0, Math.min(1.0, Math.round(importance * 10000) / 10000));
447
+ const result = stmts.insertMemory.run(content, clampedImportance, namespace || 'shared', parentId);
447
448
  const id = Number(result.lastInsertRowid);
448
449
 
449
450
  // Provenance Info handling
@@ -510,9 +511,10 @@ export function getAnyMemoryById(id) {
510
511
  * @returns {object|null} The memory row, or null if not found
511
512
  */
512
513
  export function getMemoryById(id, namespace = null) {
513
- const memory = namespace
514
- ? stmts.getByIdNs.get(id, namespace)
515
- : stmts.getById.get(id);
514
+ const ns = namespace || 'shared';
515
+ const memory = ns === 'all'
516
+ ? stmts.getById.get(id)
517
+ : stmts.getByIdNs.get(id, ns);
516
518
  if (memory) {
517
519
  memory.provenance = getProvenance(id);
518
520
  }
@@ -557,12 +559,17 @@ export function deleteMemory(id) {
557
559
  /**
558
560
  * Get the N most recently created memories.
559
561
  * @param {number} limit - Max results
560
- * @param {string|null} namespace - Namespace filter (null = all)
562
+ * @param {string|null} namespace - Namespace filter (null = shared)
561
563
  */
562
564
  export function getRecentMemories(limit = 10, namespace = null) {
563
- const rows = namespace
564
- ? stmts.getRecentNs.all(namespace, limit)
565
- : stmts.getRecent.all(limit);
565
+ if (typeof limit !== 'number' || isNaN(limit) || limit <= 0) {
566
+ throw new Error('Limit must be a positive integer.');
567
+ }
568
+ const parsedLimit = Math.floor(limit);
569
+ const ns = namespace || 'shared';
570
+ const rows = ns === 'all'
571
+ ? stmts.getRecent.all(parsedLimit)
572
+ : stmts.getRecentNs.all(ns, parsedLimit);
566
573
  rows.forEach(r => {
567
574
  r.provenance = getProvenance(r.id);
568
575
  });
@@ -572,12 +579,17 @@ export function getRecentMemories(limit = 10, namespace = null) {
572
579
  /**
573
580
  * Get the N most important memories (by importance_score).
574
581
  * @param {number} limit - Max results
575
- * @param {string|null} namespace - Namespace filter (null = all)
582
+ * @param {string|null} namespace - Namespace filter (null = shared)
576
583
  */
577
584
  export function getImportantMemories(limit = 10, namespace = null) {
578
- const rows = namespace
579
- ? stmts.getImportantNs.all(namespace, limit)
580
- : stmts.getImportant.all(limit);
585
+ if (typeof limit !== 'number' || isNaN(limit) || limit <= 0) {
586
+ throw new Error('Limit must be a positive integer.');
587
+ }
588
+ const parsedLimit = Math.floor(limit);
589
+ const ns = namespace || 'shared';
590
+ const rows = ns === 'all'
591
+ ? stmts.getImportant.all(parsedLimit)
592
+ : stmts.getImportantNs.all(ns, parsedLimit);
581
593
  rows.forEach(r => {
582
594
  r.provenance = getProvenance(r.id);
583
595
  });
@@ -633,7 +645,11 @@ export function searchKeyword(query, limit = 10) {
633
645
  * @returns {Array<{rowid: number, distance: number}>}
634
646
  */
635
647
  export function searchVector(embedding, limit = 10) {
636
- return stmts.searchVec.all(Buffer.from(embedding.buffer), limit);
648
+ if (typeof limit !== 'number' || isNaN(limit) || limit <= 0) {
649
+ throw new Error('Limit must be a positive integer.');
650
+ }
651
+ const parsedLimit = Math.floor(limit);
652
+ return stmts.searchVec.all(Buffer.from(embedding.buffer), parsedLimit);
637
653
  }
638
654
 
639
655
  // ============================================================
package/src/search.js CHANGED
@@ -33,6 +33,12 @@ let lastDataVersion = 0;
33
33
  * @returns {Promise<Array>} Ranked search results (with .attestation property attached)
34
34
  */
35
35
  export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null, skipAttestation = false) {
36
+ if (typeof limit !== 'number' || isNaN(limit) || limit <= 0) {
37
+ throw new Error('Limit must be a positive integer.');
38
+ }
39
+ const parsedLimit = Math.floor(limit);
40
+ const ns = namespace || 'shared';
41
+
36
42
  // Sync in-memory cache with external DB changes using sqlite data_version
37
43
  try {
38
44
  const currentDataVersion = db.pragma('data_version', { simple: true });
@@ -46,7 +52,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
46
52
 
47
53
  // --- Check LRU cache first (Feature 1) ---
48
54
  // Include namespace in cache key to prevent cross-namespace cache hits
49
- const cacheKey = LRUCache.key(`${namespace || 'all'}:${queryText}`, limit);
55
+ const cacheKey = LRUCache.key(`${ns}:${queryText}`, parsedLimit);
50
56
  const cached = searchCache.get(cacheKey);
51
57
  if (cached) {
52
58
  console.error(`[persyst-cache] Cache HIT for query: "${queryText.slice(0, 50)}..."`);
@@ -54,12 +60,12 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
54
60
  }
55
61
 
56
62
  // --- Step 1: Keyword search (fast, exact matches) ---
57
- const keywordHits = searchKeyword(queryText, limit * 2);
63
+ const keywordHits = searchKeyword(queryText, parsedLimit * 2);
58
64
  const keywordIds = new Set(keywordHits.map(r => r.id));
59
65
 
60
66
  // --- Step 2: Semantic search (meaning-based) ---
61
67
  const queryEmbedding = await generateEmbedding(queryText);
62
- const vecHits = searchVector(queryEmbedding, limit * 2);
68
+ const vecHits = searchVector(queryEmbedding, parsedLimit * 2);
63
69
 
64
70
  const semanticResults = vecHits.map(r => ({
65
71
  id: r.rowid,
@@ -99,7 +105,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
99
105
  const finalResults = combined
100
106
  .map(r => {
101
107
  // Use namespace-aware getMemoryById to filter by agent namespace
102
- const memory = getMemoryById(r.id, namespace);
108
+ const memory = getMemoryById(r.id, ns);
103
109
  if (!memory) return null; // Memory was archived, deleted, or not in namespace
104
110
 
105
111
  // Boost memory access metrics
@@ -141,7 +147,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
141
147
  finalResults.sort((a, b) => parseFloat(b.hybrid_score) - parseFloat(a.hybrid_score));
142
148
 
143
149
  // --- Step 5: Apply MMR for diverse retrieval (Feature 3) ---
144
- const mmrResults = applyMMR(finalResults, limit);
150
+ const mmrResults = applyMMR(finalResults, parsedLimit);
145
151
 
146
152
  // Generate cryptographic attestation for audit trails (skip if called internally)
147
153
  let attestation = null;
package/src/tools.js CHANGED
@@ -290,7 +290,7 @@ export function registerTools(server) {
290
290
  'Search memories using hybrid keyword + semantic search with cryptographic attestation. CRITICAL: Call this tool at the start of a session or task to retrieve relevant user preferences, coding guidelines, and past decisions.',
291
291
  {
292
292
  query: z.string().describe('What to search for'),
293
- limit: z.number().default(5).describe('Max results (default: 5)'),
293
+ limit: z.number().int().min(1).default(5).describe('Max results (default: 5)'),
294
294
  agent_id: z.string().optional().describe('Agent ID — filters results to this agent\'s namespace + shared'),
295
295
  session_id: z.string().optional().describe('Session ID')
296
296
  },
@@ -415,7 +415,7 @@ export function registerTools(server) {
415
415
  'get_recent_memories',
416
416
  'Get the most recently created memories, newest first. Filtered by agent namespace if agent_id is provided.',
417
417
  {
418
- limit: z.number().default(10).describe('How many to return (default: 10)'),
418
+ limit: z.number().int().min(1).default(10).describe('How many to return (default: 10)'),
419
419
  agent_id: z.string().optional().describe('Agent ID — filters to this agent\'s namespace + shared')
420
420
  },
421
421
  async ({ limit, agent_id }) => {
@@ -434,7 +434,7 @@ export function registerTools(server) {
434
434
  'get_important_memories',
435
435
  'Get memories ranked by importance score, highest first. Filtered by agent namespace if agent_id is provided.',
436
436
  {
437
- limit: z.number().default(10).describe('How many to return (default: 10)'),
437
+ limit: z.number().int().min(1).default(10).describe('How many to return (default: 10)'),
438
438
  agent_id: z.string().optional().describe('Agent ID — filters to this agent\'s namespace + shared')
439
439
  },
440
440
  async ({ limit, agent_id }) => {