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/src/tools.js CHANGED
@@ -33,6 +33,7 @@ import db, {
33
33
  boostMemory,
34
34
  logContradiction,
35
35
  getProvenance,
36
+ incrementAgentStat,
36
37
  getAllAgentStats,
37
38
  getAttestationsByDateRange,
38
39
  getMemoryHistoryChain,
@@ -52,8 +53,8 @@ import { searchCache } from './cache.js';
52
53
  // CONSTANTS
53
54
  // ============================================================
54
55
 
55
- /** Maximum allowed memory content length (50,000 characters) */
56
- const MAX_MEMORY_CONTENT_LENGTH = 50000;
56
+ /** Maximum allowed memory content length (10,000 characters) */
57
+ const MAX_MEMORY_CONTENT_LENGTH = 10000;
57
58
 
58
59
  /** Minimum content length (must have actual content) */
59
60
  const MIN_MEMORY_CONTENT_LENGTH = 1;
@@ -99,6 +100,148 @@ function validateMemoryContent(content) {
99
100
  return { valid: true };
100
101
  }
101
102
 
103
+ /**
104
+ * Internal logic for storing a new memory (dedup, vector creation, contradiction detection).
105
+ * Shared by both the stdio MCP tool and the HTTP Gateway server.
106
+ */
107
+ export async function addMemoryInternal({ content, importance = 1.0, agent_id, session_id, shared = true }) {
108
+ try {
109
+ const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
110
+
111
+ // Bug 7 + Feature 4: Validate content size
112
+ const validation = validateMemoryContent(content);
113
+ if (!validation.valid) {
114
+ return { error: validation.error };
115
+ }
116
+
117
+ // Derive namespace from agent_id and shared flag
118
+ const namespace = (shared || !normalizedAgentId) ? 'shared' : normalizedAgentId;
119
+
120
+ // Deduplication check (namespace-aware)
121
+ const existing = getMemoryByContent(content, namespace);
122
+ if (existing) {
123
+ // Re-attribute provenance to the calling agent if it was previously auto-attributed to log-watcher
124
+ const prov = getProvenance(existing.id);
125
+ if (prov && (prov.source_id === 'antigravity-worker' || prov.source_id === 'user-dialogue') && normalizedAgentId) {
126
+ try {
127
+ db.prepare("UPDATE provenance SET source_type = 'agent', source_id = ?, confidence = 1.0 WHERE memory_id = ?")
128
+ .run(normalizedAgentId, existing.id);
129
+ incrementAgentStat(normalizedAgentId, 'created');
130
+ } catch (e) {
131
+ console.error(`[persyst] Re-attribute provenance error: ${e.message}`);
132
+ }
133
+ }
134
+ boostMemory(existing.id);
135
+ return {
136
+ success: true,
137
+ id: existing.id,
138
+ namespace,
139
+ message: `Memory #${existing.id} already exists. Boosted importance.`
140
+ };
141
+ }
142
+
143
+ const id = insertMemory(content, importance, {
144
+ source_type: normalizedAgentId ? 'agent' : 'manual',
145
+ source_id: normalizedAgentId,
146
+ confidence: 1.0
147
+ }, namespace);
148
+
149
+ const embedding = await generateEmbedding(content);
150
+ insertVector(id, embedding);
151
+
152
+ // Feature 1: Invalidate search cache on write
153
+ searchCache.invalidate();
154
+
155
+ // Feature 2: Contradiction Detection
156
+ let contradictions = [];
157
+ try {
158
+ const similarHits = searchVector(embedding, 20);
159
+ for (const hit of similarHits) {
160
+ const hitId = Number(hit.rowid);
161
+ if (hitId === id) continue; // Skip self
162
+
163
+ const sim = Math.max(0, 1 - (hit.distance * hit.distance) / 2);
164
+ if (sim > 0.70) {
165
+ const existingMemory = getMemoryById(hitId, namespace);
166
+ if (!existingMemory) continue;
167
+
168
+ const jaccard = jaccardDistance(content, existingMemory.content);
169
+ // Contradiction: similar topic (high similarity), but differing key terms
170
+ if (jaccard > 0 && jaccard < 0.65) {
171
+ // Fetch provenances for trust calculation
172
+ const oldProv = getProvenance(hitId);
173
+ let oldReputation = 1.0;
174
+ if (oldProv && oldProv.source_type === 'agent' && oldProv.source_id) {
175
+ const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(oldProv.source_id);
176
+ if (agentRow) oldReputation = agentRow.reputation_score;
177
+ }
178
+
179
+ let newReputation = 1.0;
180
+ if (normalizedAgentId) {
181
+ const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(normalizedAgentId);
182
+ if (agentRow) newReputation = agentRow.reputation_score;
183
+ }
184
+
185
+ const trustOld = (oldProv ? oldProv.confidence : 1.0) * oldReputation;
186
+ const trustNew = 1.0 * newReputation; // New confidence is 1.0
187
+
188
+ const isSelfUpdate = oldProv && oldProv.source_type === 'agent' && oldProv.source_id === normalizedAgentId;
189
+
190
+ if (isSelfUpdate || trustNew > trustOld) {
191
+ // New is preferred
192
+ logContradiction(hitId, id, `Auto-detected contradiction: new memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
193
+ contradictions.push({
194
+ old_memory_id: hitId,
195
+ old_content_preview: existingMemory.content.slice(0, 100),
196
+ similarity: sim.toFixed(4),
197
+ content_difference: jaccard.toFixed(4),
198
+ resolution: 'replaced_old'
199
+ });
200
+ } else {
201
+ // Old is preferred
202
+ logContradiction(id, hitId, `Auto-detected contradiction: existing memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
203
+ contradictions.push({
204
+ old_memory_id: hitId,
205
+ old_content_preview: existingMemory.content.slice(0, 100),
206
+ similarity: sim.toFixed(4),
207
+ content_difference: jaccard.toFixed(4),
208
+ resolution: 'kept_old'
209
+ });
210
+ break; // New memory was archived, stop contradiction check
211
+ }
212
+ }
213
+ }
214
+ }
215
+ } catch (e) {
216
+ console.error(`[persyst] Contradiction detection error: ${e.message}`);
217
+ }
218
+
219
+ const result = { success: true, id, namespace, message: `Memory #${id} stored` };
220
+ if (contradictions.length > 0) {
221
+ result.contradictions_detected = contradictions;
222
+ result.message += `. Detected ${contradictions.length} contradiction(s) — older memories archived.`;
223
+ }
224
+
225
+ return result;
226
+ } catch (err) {
227
+ return { error: err.message };
228
+ }
229
+ }
230
+
231
+ const toolHandlers = new Map();
232
+
233
+ /**
234
+ * Programmatically execute any registered MCP tool.
235
+ * Used by the HTTP Gateway server to route requests to tool handlers.
236
+ */
237
+ export async function executeToolInternal(name, args) {
238
+ const handler = toolHandlers.get(name);
239
+ if (!handler) {
240
+ throw new Error(`Tool ${name} not found`);
241
+ }
242
+ return await handler(args);
243
+ }
244
+
102
245
  /**
103
246
  * Register all MCP tools on the server.
104
247
  * @param {McpServer} server - The MCP server instance
@@ -108,6 +251,11 @@ export function registerTools(server) {
108
251
  let count = 0;
109
252
  const originalTool = server.tool.bind(server);
110
253
  server.tool = (...args) => {
254
+ const name = args[0];
255
+ const handler = args[args.length - 1];
256
+ if (typeof handler === 'function') {
257
+ toolHandlers.set(name, handler);
258
+ }
111
259
  originalTool(...args);
112
260
  count++;
113
261
  };
@@ -128,114 +276,11 @@ export function registerTools(server) {
128
276
  shared: z.boolean().default(true).describe('If true, memory is visible to all agents. If false, only visible to this agent.')
129
277
  },
130
278
  async ({ content, importance, agent_id, session_id, shared }) => {
131
- try {
132
- // Bug 7 + Feature 4: Validate content size
133
- const validation = validateMemoryContent(content);
134
- if (!validation.valid) {
135
- return text({ error: validation.error });
136
- }
137
-
138
- // Derive namespace from agent_id and shared flag
139
- const namespace = (shared || !agent_id) ? 'shared' : agent_id;
140
-
141
- // Deduplication check (namespace-aware)
142
- const existing = getMemoryByContent(content, namespace);
143
- if (existing) {
144
- boostMemory(existing.id);
145
- return text({
146
- success: true,
147
- id: existing.id,
148
- namespace,
149
- message: `Memory #${existing.id} already exists. Boosted importance.`
150
- });
151
- }
152
-
153
- const id = insertMemory(content, importance, {
154
- source_type: agent_id ? 'agent' : 'manual',
155
- source_id: agent_id || null,
156
- confidence: 1.0
157
- }, namespace);
158
-
159
- const embedding = await generateEmbedding(content);
160
- insertVector(id, embedding);
161
-
162
- // Feature 1: Invalidate search cache on write
163
- searchCache.invalidate();
164
-
165
- // Feature 2: Contradiction Detection
166
- let contradictions = [];
167
- try {
168
- const similarHits = searchVector(embedding, 3);
169
- for (const hit of similarHits) {
170
- const hitId = Number(hit.rowid);
171
- if (hitId === id) continue; // Skip self
172
-
173
- const sim = Math.max(0, 1 - (hit.distance * hit.distance) / 2);
174
- if (sim > 0.75) {
175
- const existingMemory = getMemoryById(hitId, namespace);
176
- if (!existingMemory) continue;
177
-
178
- const jaccard = jaccardDistance(content, existingMemory.content);
179
- // Contradiction: similar topic (high similarity), but differing key terms
180
- if (jaccard > 0 && jaccard < 0.5) {
181
- // Fetch provenances for trust calculation
182
- const oldProv = getProvenance(hitId);
183
- let oldReputation = 1.0;
184
- if (oldProv && oldProv.source_type === 'agent' && oldProv.source_id) {
185
- const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(oldProv.source_id);
186
- if (agentRow) oldReputation = agentRow.reputation_score;
187
- }
188
-
189
- let newReputation = 1.0;
190
- if (agent_id) {
191
- const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(agent_id);
192
- if (agentRow) newReputation = agentRow.reputation_score;
193
- }
194
-
195
- const trustOld = (oldProv ? oldProv.confidence : 1.0) * oldReputation;
196
- const trustNew = 1.0 * newReputation; // New confidence is 1.0
197
-
198
- const isSelfUpdate = oldProv && oldProv.source_type === 'agent' && oldProv.source_id === agent_id;
199
-
200
- if (isSelfUpdate || trustNew > trustOld) {
201
- // New is preferred
202
- logContradiction(hitId, id, `Auto-detected contradiction: new memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
203
- contradictions.push({
204
- old_memory_id: hitId,
205
- old_content_preview: existingMemory.content.slice(0, 100),
206
- similarity: sim.toFixed(4),
207
- content_difference: jaccard.toFixed(4),
208
- resolution: 'replaced_old'
209
- });
210
- } else {
211
- // Old is preferred
212
- logContradiction(id, hitId, `Auto-detected contradiction: existing memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
213
- contradictions.push({
214
- old_memory_id: hitId,
215
- old_content_preview: existingMemory.content.slice(0, 100),
216
- similarity: sim.toFixed(4),
217
- content_difference: jaccard.toFixed(4),
218
- resolution: 'kept_old'
219
- });
220
- break; // New memory was archived, stop contradiction check
221
- }
222
- }
223
- }
224
- }
225
- } catch (e) {
226
- console.error(`[persyst] Contradiction detection error: ${e.message}`);
227
- }
228
-
229
- const result = { success: true, id, namespace, message: `Memory #${id} stored` };
230
- if (contradictions.length > 0) {
231
- result.contradictions_detected = contradictions;
232
- result.message += `. Detected ${contradictions.length} contradiction(s) — older memories archived.`;
233
- }
234
-
235
- return text(result);
236
- } catch (err) {
237
- return text({ error: err.message });
279
+ const res = await addMemoryInternal({ content, importance, agent_id, session_id, shared });
280
+ if (res.error) {
281
+ return text({ error: res.error });
238
282
  }
283
+ return text(res);
239
284
  }
240
285
  );
241
286
 
@@ -295,6 +340,8 @@ export function registerTools(server) {
295
340
  },
296
341
  async ({ id, content, agent_id }) => {
297
342
  try {
343
+ const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
344
+
298
345
  // Bug 7 + Feature 4: Validate content size
299
346
  const validation = validateMemoryContent(content);
300
347
  if (!validation.valid) {
@@ -304,12 +351,22 @@ export function registerTools(server) {
304
351
  const oldMemory = getMemory(id);
305
352
  if (!oldMemory) return text({ error: `Memory #${id} not found` });
306
353
 
354
+ // Retrieve old agent_id from provenance
355
+ const oldProv = getProvenance(id);
356
+ const resolvedAgentId = normalizedAgentId || (oldProv && oldProv.source_type === 'agent' ? oldProv.source_id : null);
357
+
307
358
  // Insert new version
308
- const newId = insertMemory(content, oldMemory.importance_score, {
309
- source_type: agent_id ? 'agent' : 'manual',
310
- source_id: agent_id || null,
311
- confidence: 1.0
312
- });
359
+ const newId = insertMemory(
360
+ content,
361
+ oldMemory.importance_score,
362
+ {
363
+ source_type: resolvedAgentId ? 'agent' : 'manual',
364
+ source_id: resolvedAgentId,
365
+ confidence: 1.0
366
+ },
367
+ oldMemory.namespace || 'shared',
368
+ id
369
+ );
313
370
 
314
371
  const embedding = await generateEmbedding(content);
315
372
  insertVector(newId, embedding);
@@ -545,13 +602,29 @@ export function registerTools(server) {
545
602
  hits = searchAllMemoriesFts(query, 5);
546
603
  }
547
604
 
605
+ // Fallback to LIKE query on memories content if FTS is empty or fails
606
+ if (hits.length === 0) {
607
+ try {
608
+ const likeRows = db.prepare("SELECT id FROM memories WHERE content LIKE ? LIMIT 5").all(`%${query}%`);
609
+ hits = likeRows;
610
+ } catch (_) {}
611
+ }
612
+
548
613
  if (hits.length === 0) {
549
614
  return text({ message: 'No memories matching query found.' });
550
615
  }
551
616
 
552
617
  const histories = {};
618
+ const seenChainKeys = new Set();
553
619
  for (const hit of hits) {
554
620
  const chain = getMemoryHistoryChain(hit.id);
621
+ if (chain.length === 0) continue;
622
+
623
+ // Deduplicate chains to prevent duplicate entries in history response
624
+ const chainKey = chain.map(c => c.id).sort((a, b) => a - b).join(',');
625
+ if (seenChainKeys.has(chainKey)) continue;
626
+ seenChainKeys.add(chainKey);
627
+
555
628
  // Decorate chain versions with semantic diffs from the previous version
556
629
  for (let idx = 0; idx < chain.length; idx++) {
557
630
  if (idx > 0) {