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/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 (
|
|
56
|
-
const MAX_MEMORY_CONTENT_LENGTH =
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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) {
|