persyst-mcp 2.2.5 → 2.2.7
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 +103 -114
- package/bin/export.js +4 -4
- package/bin/extract.js +8 -8
- package/bin/import.js +15 -15
- package/bin/init.js +185 -38
- package/bin/mcp.js +3 -0
- package/bin/monitor.js +511 -0
- package/bin/setup.js +9 -9
- package/index.js +31 -11
- package/package.json +10 -11
- package/src/attestation.js +49 -28
- package/src/cache.js +3 -1
- package/src/database.js +227 -34
- package/src/embeddings.js +4 -2
- package/src/events.js +2 -0
- package/src/extractor-heuristic.js +5 -2
- package/src/sdk.js +4 -3
- package/src/search.js +55 -84
- package/src/server.js +884 -723
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +52 -0
- package/src/tools.js +98 -53
- package/src/watcher.js +157 -49
package/src/setup-wasm.js
CHANGED
|
@@ -9,62 +9,57 @@ const require = createRequire(import.meta.url);
|
|
|
9
9
|
const onnxWebPath = require.resolve('onnxruntime-web');
|
|
10
10
|
const wasmDir = path.dirname(onnxWebPath);
|
|
11
11
|
|
|
12
|
-
// Redirect native Node session creation to WebAssembly session creation
|
|
13
12
|
ONNX_NODE.InferenceSession.create = ONNX_WEB.InferenceSession.create;
|
|
14
13
|
|
|
15
|
-
// Override URL.createObjectURL to return file URL of the existing local file
|
|
16
14
|
const originalCreateObjectURL = URL.createObjectURL;
|
|
17
|
-
|
|
15
|
+
const patchedCreateObjectURL = (blob) => {
|
|
18
16
|
const type = blob.type || '';
|
|
19
17
|
if (type.includes('javascript') || type.includes('mjs')) {
|
|
20
18
|
const filePath = path.join(wasmDir, 'ort-wasm-simd-threaded.asyncify.mjs');
|
|
21
|
-
|
|
22
|
-
return fileUrl;
|
|
19
|
+
return pathToFileURL(filePath).href;
|
|
23
20
|
}
|
|
24
21
|
return originalCreateObjectURL(blob);
|
|
25
22
|
};
|
|
23
|
+
URL.createObjectURL = patchedCreateObjectURL;
|
|
24
|
+
|
|
25
|
+
function readLocalFile(filePath, urlStr) {
|
|
26
|
+
const normalized = path.normalize(filePath);
|
|
27
|
+
const buffer = fs.readFileSync(normalized);
|
|
28
|
+
let contentType = 'application/octet-stream';
|
|
29
|
+
if (normalized.endsWith('.wasm')) contentType = 'application/wasm';
|
|
30
|
+
else if (normalized.endsWith('.mjs') || normalized.endsWith('.js')) contentType = 'text/javascript';
|
|
31
|
+
else if (normalized.endsWith('.onnx') || normalized.endsWith('.ort')) contentType = 'application/octet-stream';
|
|
32
|
+
return new Response(buffer, {
|
|
33
|
+
status: 200,
|
|
34
|
+
statusText: 'OK',
|
|
35
|
+
headers: { 'Content-Type': contentType }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
26
38
|
|
|
27
|
-
// Override global fetch to load ONNX WASM binaries and model files from local disk
|
|
28
39
|
const originalFetch = globalThis.fetch;
|
|
29
|
-
|
|
40
|
+
const patchedFetch = async (url, options) => {
|
|
30
41
|
const urlStr = typeof url === 'string' ? url : url.url;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let filePath = '';
|
|
34
|
-
|
|
35
|
-
if (urlStr.startsWith('file://')) {
|
|
36
|
-
isLocal = true;
|
|
37
|
-
filePath = fileURLToPath(urlStr);
|
|
38
|
-
} else if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://') && !urlStr.startsWith('data:')) {
|
|
39
|
-
isLocal = true;
|
|
40
|
-
filePath = urlStr;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Intercept onnxruntime-web CDN URLs and route them to node_modules/onnxruntime-web/dist
|
|
42
|
+
|
|
43
|
+
// onnxruntime-web WASM binaries — resolve from node_modules
|
|
44
44
|
if (urlStr.includes('onnxruntime-web') || urlStr.includes('ort-wasm')) {
|
|
45
|
-
isLocal = true;
|
|
46
45
|
const filename = urlStr.split('/').pop().split('?')[0].split('#')[0];
|
|
47
|
-
|
|
46
|
+
return readLocalFile(path.join(wasmDir, filename), urlStr);
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
|
|
49
|
+
// file:// URLs — Node.js fetch does not support them natively
|
|
50
|
+
if (urlStr.startsWith('file://')) {
|
|
51
|
+
return readLocalFile(fileURLToPath(urlStr), urlStr);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// fallback for any non-http/https/data URL (onnxruntime internal schemes, bare paths)
|
|
55
|
+
if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://') && !urlStr.startsWith('data:')) {
|
|
52
56
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else if (filePath.endsWith('.mjs') || filePath.endsWith('.js')) contentType = 'text/javascript';
|
|
57
|
-
|
|
58
|
-
return new Response(buffer, {
|
|
59
|
-
status: 200,
|
|
60
|
-
statusText: 'OK',
|
|
61
|
-
headers: { 'Content-Type': contentType }
|
|
62
|
-
});
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.error('[persyst] Failed to read local file:', filePath, err.message);
|
|
65
|
-
throw err;
|
|
57
|
+
return readLocalFile(urlStr, urlStr);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
throw e;
|
|
66
60
|
}
|
|
67
61
|
}
|
|
68
|
-
|
|
62
|
+
|
|
69
63
|
return originalFetch(url, options);
|
|
70
64
|
};
|
|
65
|
+
globalThis.fetch = patchedFetch;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* text-utils.js — Shared text-processing helpers used across Persyst.
|
|
3
|
+
*
|
|
4
|
+
* Keeping these in one place avoids duplicated logic and divergent behavior
|
|
5
|
+
* between modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute Jaccard similarity between two text strings.
|
|
10
|
+
* Uses word-level tokenization for efficiency.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} a - First text
|
|
13
|
+
* @param {string} b - Second text
|
|
14
|
+
* @returns {number} Similarity score between 0 and 1
|
|
15
|
+
*/
|
|
16
|
+
export function jaccardSimilarity(a, b) {
|
|
17
|
+
if (!a || !b) return 0;
|
|
18
|
+
|
|
19
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
20
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
21
|
+
|
|
22
|
+
let intersection = 0;
|
|
23
|
+
for (const word of wordsA) {
|
|
24
|
+
if (wordsB.has(word)) intersection++;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
28
|
+
return union === 0 ? 0 : intersection / union;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compute Jaccard distance between two text strings.
|
|
33
|
+
* Distance = 1 - similarity, so 0 means identical and 1 means completely different.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} a - First text
|
|
36
|
+
* @param {string} b - Second text
|
|
37
|
+
* @returns {number} Distance score between 0 and 1
|
|
38
|
+
*/
|
|
39
|
+
export function jaccardDistance(a, b) {
|
|
40
|
+
return 1 - jaccardSimilarity(a, b);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Log informational messages to stderr only when PERSYST_DEBUG or DEBUG is enabled.
|
|
45
|
+
* Prevents MCP hosts (Cursor, Antigravity, VS Code) from treating startup info logs as MCP errors.
|
|
46
|
+
*/
|
|
47
|
+
export function logInfo(...args) {
|
|
48
|
+
if (process.env.PERSYST_DEBUG || process.env.DEBUG) {
|
|
49
|
+
console.error(...args);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
package/src/tools.js
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { generateEmbedding } from './embeddings.js';
|
|
16
16
|
import db, {
|
|
17
|
+
stmts,
|
|
17
18
|
insertMemory,
|
|
18
19
|
insertVector,
|
|
20
|
+
redactSecrets,
|
|
19
21
|
getMemory,
|
|
20
22
|
updateMemoryContent,
|
|
21
23
|
deleteMemory,
|
|
@@ -45,6 +47,7 @@ import db, {
|
|
|
45
47
|
getNamespaceStats
|
|
46
48
|
} from './database.js';
|
|
47
49
|
import { searchHybrid, getOptimizedContext, consolidateMemories } from './search.js';
|
|
50
|
+
import { jaccardDistance } from './text-utils.js';
|
|
48
51
|
import { getRecentCommits } from './git.js';
|
|
49
52
|
import { verifyChainIntegrity } from './attestation.js';
|
|
50
53
|
import { searchCache } from './cache.js';
|
|
@@ -109,24 +112,28 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
|
|
|
109
112
|
try {
|
|
110
113
|
const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
|
|
111
114
|
|
|
115
|
+
// Redact secrets/credentials on write
|
|
116
|
+
const redactedContent = redactSecrets(content);
|
|
117
|
+
|
|
112
118
|
// Bug 7 + Feature 4: Validate content size
|
|
113
|
-
const validation = validateMemoryContent(
|
|
119
|
+
const validation = validateMemoryContent(redactedContent);
|
|
114
120
|
if (!validation.valid) {
|
|
115
121
|
return { error: validation.error };
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
// Derive namespace from agent_id and shared flag
|
|
119
|
-
const
|
|
124
|
+
// Derive namespace from agent_id, project env, and shared flag
|
|
125
|
+
const project = process.env.PERSYST_PROJECT;
|
|
126
|
+
const defaultNs = project || 'shared';
|
|
127
|
+
const namespace = (shared && !project) ? 'shared' : (normalizedAgentId || defaultNs);
|
|
120
128
|
|
|
121
129
|
// Deduplication check (namespace-aware)
|
|
122
|
-
const existing = getMemoryByContent(
|
|
130
|
+
const existing = getMemoryByContent(redactedContent, namespace);
|
|
123
131
|
if (existing) {
|
|
124
132
|
// Re-attribute provenance to the calling agent if it was previously auto-attributed to log-watcher
|
|
125
133
|
const prov = getProvenance(existing.id);
|
|
126
134
|
if (prov && (prov.source_id === 'antigravity-worker' || prov.source_id === 'user-dialogue') && normalizedAgentId) {
|
|
127
135
|
try {
|
|
128
|
-
|
|
129
|
-
.run(normalizedAgentId, existing.id);
|
|
136
|
+
stmts.updateProvenanceOwner.run(normalizedAgentId, existing.id);
|
|
130
137
|
incrementAgentStat(normalizedAgentId, 'created');
|
|
131
138
|
} catch (e) {
|
|
132
139
|
console.error(`[persyst] Re-attribute provenance error: ${e.message}`);
|
|
@@ -141,20 +148,20 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
|
|
|
141
148
|
};
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
const id = insertMemory(
|
|
151
|
+
const id = insertMemory(redactedContent, importance, {
|
|
145
152
|
source_type: normalizedAgentId ? 'agent' : 'manual',
|
|
146
153
|
source_id: normalizedAgentId,
|
|
147
154
|
confidence: 1.0
|
|
148
155
|
}, namespace);
|
|
149
156
|
|
|
150
|
-
const embedding = await generateEmbedding(
|
|
157
|
+
const embedding = await generateEmbedding(redactedContent);
|
|
151
158
|
insertVector(id, embedding);
|
|
152
159
|
|
|
153
160
|
// Feature 1: Invalidate search cache on write
|
|
154
161
|
searchCache.invalidate();
|
|
155
162
|
|
|
156
163
|
// Broadcast to SSE subscribers (HTTP gateway + SSE clients)
|
|
157
|
-
memoryEventBus.emit('memory_added', { id, content, namespace, source: normalizedAgentId || 'manual' });
|
|
164
|
+
memoryEventBus.emit('memory_added', { id, content: redactedContent, namespace, source: normalizedAgentId || 'manual' });
|
|
158
165
|
|
|
159
166
|
// Feature 2: Contradiction Detection
|
|
160
167
|
let contradictions = [];
|
|
@@ -169,20 +176,20 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
|
|
|
169
176
|
const existingMemory = getMemoryById(hitId, namespace);
|
|
170
177
|
if (!existingMemory) continue;
|
|
171
178
|
|
|
172
|
-
const jaccard = jaccardDistance(
|
|
179
|
+
const jaccard = jaccardDistance(redactedContent, existingMemory.content);
|
|
173
180
|
// Contradiction: similar topic (high similarity), but differing key terms
|
|
174
181
|
if (jaccard > 0 && jaccard < 0.65) {
|
|
175
182
|
// Fetch provenances for trust calculation
|
|
176
183
|
const oldProv = getProvenance(hitId);
|
|
177
184
|
let oldReputation = 1.0;
|
|
178
185
|
if (oldProv && oldProv.source_type === 'agent' && oldProv.source_id) {
|
|
179
|
-
const agentRow =
|
|
186
|
+
const agentRow = stmts.getReputationScore.get(oldProv.source_id);
|
|
180
187
|
if (agentRow) oldReputation = agentRow.reputation_score;
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
let newReputation = 1.0;
|
|
184
191
|
if (normalizedAgentId) {
|
|
185
|
-
const agentRow =
|
|
192
|
+
const agentRow = stmts.getReputationScore.get(normalizedAgentId);
|
|
186
193
|
if (agentRow) newReputation = agentRow.reputation_score;
|
|
187
194
|
}
|
|
188
195
|
|
|
@@ -191,7 +198,11 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
|
|
|
191
198
|
|
|
192
199
|
const isSelfUpdate = oldProv && oldProv.source_type === 'agent' && oldProv.source_id === normalizedAgentId;
|
|
193
200
|
|
|
194
|
-
if (isSelfUpdate
|
|
201
|
+
if (isSelfUpdate) {
|
|
202
|
+
continue; // Same agent: treat as complementary, not contradictory
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (trustNew > trustOld) {
|
|
195
206
|
// New is preferred
|
|
196
207
|
logContradiction(hitId, id, `Auto-detected contradiction: new memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
|
|
197
208
|
contradictions.push({
|
|
@@ -300,9 +311,22 @@ export function registerTools(server) {
|
|
|
300
311
|
},
|
|
301
312
|
async ({ query, limit, agent_id, session_id }) => {
|
|
302
313
|
try {
|
|
303
|
-
// Derive namespace from agent_id
|
|
304
|
-
const namespace = agent_id || null;
|
|
314
|
+
// Derive namespace from agent_id or PERSYST_PROJECT env
|
|
315
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
305
316
|
const results = await searchHybrid(query, limit, agent_id, session_id, namespace);
|
|
317
|
+
|
|
318
|
+
// Broadcast retrieval event to SSE subscribers and monitor
|
|
319
|
+
if (results && results.length > 0) {
|
|
320
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
321
|
+
tool: 'search_memories',
|
|
322
|
+
query,
|
|
323
|
+
count: results.length,
|
|
324
|
+
agent_id: agent_id || 'unknown',
|
|
325
|
+
namespace: namespace || 'shared',
|
|
326
|
+
memory_ids: results.map(r => r.id)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
306
330
|
return text({
|
|
307
331
|
results,
|
|
308
332
|
count: results.length,
|
|
@@ -320,12 +344,25 @@ export function registerTools(server) {
|
|
|
320
344
|
'get_memory',
|
|
321
345
|
'Get a specific memory by its ID. Boosts its importance automatically.',
|
|
322
346
|
{
|
|
323
|
-
id: z.number().describe('Memory ID to retrieve')
|
|
347
|
+
id: z.number().describe('Memory ID to retrieve'),
|
|
348
|
+
agent_id: z.string().optional().describe('Agent ID — restricts access to this agent\'s namespace + shared')
|
|
324
349
|
},
|
|
325
|
-
async ({ id }) => {
|
|
350
|
+
async ({ id, agent_id }) => {
|
|
326
351
|
try {
|
|
327
|
-
const
|
|
352
|
+
const namespace = agent_id ? agent_id.toLowerCase() : null;
|
|
353
|
+
const memory = getMemory(id, namespace);
|
|
328
354
|
if (!memory) return text({ error: `Memory #${id} not found` });
|
|
355
|
+
|
|
356
|
+
// Broadcast retrieval event
|
|
357
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
358
|
+
tool: 'get_memory',
|
|
359
|
+
query: `#${id}`,
|
|
360
|
+
count: 1,
|
|
361
|
+
agent_id: agent_id || 'unknown',
|
|
362
|
+
namespace: memory.namespace || 'shared',
|
|
363
|
+
memory_ids: [id]
|
|
364
|
+
});
|
|
365
|
+
|
|
329
366
|
return text(memory);
|
|
330
367
|
} catch (err) {
|
|
331
368
|
return text({ error: err.message });
|
|
@@ -346,13 +383,17 @@ export function registerTools(server) {
|
|
|
346
383
|
try {
|
|
347
384
|
const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
|
|
348
385
|
|
|
386
|
+
// Redact secrets/credentials on update
|
|
387
|
+
const redactedContent = redactSecrets(content);
|
|
388
|
+
|
|
349
389
|
// Bug 7 + Feature 4: Validate content size
|
|
350
|
-
const validation = validateMemoryContent(
|
|
390
|
+
const validation = validateMemoryContent(redactedContent);
|
|
351
391
|
if (!validation.valid) {
|
|
352
392
|
return text({ error: validation.error });
|
|
353
393
|
}
|
|
354
394
|
|
|
355
|
-
const
|
|
395
|
+
const namespace = normalizedAgentId;
|
|
396
|
+
const oldMemory = getMemory(id, namespace);
|
|
356
397
|
if (!oldMemory) return text({ error: `Memory #${id} not found` });
|
|
357
398
|
|
|
358
399
|
// Retrieve old agent_id from provenance
|
|
@@ -361,7 +402,7 @@ export function registerTools(server) {
|
|
|
361
402
|
|
|
362
403
|
// Insert new version
|
|
363
404
|
const newId = insertMemory(
|
|
364
|
-
|
|
405
|
+
redactedContent,
|
|
365
406
|
oldMemory.importance_score,
|
|
366
407
|
{
|
|
367
408
|
source_type: resolvedAgentId ? 'agent' : 'manual',
|
|
@@ -372,7 +413,7 @@ export function registerTools(server) {
|
|
|
372
413
|
id
|
|
373
414
|
);
|
|
374
415
|
|
|
375
|
-
const embedding = await generateEmbedding(
|
|
416
|
+
const embedding = await generateEmbedding(redactedContent);
|
|
376
417
|
insertVector(newId, embedding);
|
|
377
418
|
|
|
378
419
|
// Record contradiction and archive the old one
|
|
@@ -381,6 +422,9 @@ export function registerTools(server) {
|
|
|
381
422
|
// Feature 1: Invalidate search cache on write
|
|
382
423
|
searchCache.invalidate();
|
|
383
424
|
|
|
425
|
+
// Broadcast update to SSE subscribers
|
|
426
|
+
memoryEventBus.emit('memory_updated', { old_id: id, new_id: newId, namespace: oldMemory.namespace || 'shared' });
|
|
427
|
+
|
|
384
428
|
return text({
|
|
385
429
|
success: true,
|
|
386
430
|
id: newId,
|
|
@@ -397,10 +441,15 @@ export function registerTools(server) {
|
|
|
397
441
|
'delete_memory',
|
|
398
442
|
'Permanently delete a memory by its ID.',
|
|
399
443
|
{
|
|
400
|
-
id: z.number().describe('Memory ID to delete')
|
|
444
|
+
id: z.number().describe('Memory ID to delete'),
|
|
445
|
+
agent_id: z.string().optional().describe('Agent ID — restricts deletion to this agent\'s namespace + shared')
|
|
401
446
|
},
|
|
402
|
-
async ({ id }) => {
|
|
447
|
+
async ({ id, agent_id }) => {
|
|
403
448
|
try {
|
|
449
|
+
const namespace = agent_id ? agent_id.toLowerCase() : null;
|
|
450
|
+
const memory = getMemory(id, namespace);
|
|
451
|
+
if (!memory) return text({ error: `Memory #${id} not found` });
|
|
452
|
+
|
|
404
453
|
const deleted = deleteMemory(id);
|
|
405
454
|
if (!deleted) return text({ error: `Memory #${id} not found` });
|
|
406
455
|
|
|
@@ -408,7 +457,7 @@ export function registerTools(server) {
|
|
|
408
457
|
searchCache.invalidate();
|
|
409
458
|
|
|
410
459
|
// Broadcast deletion to SSE subscribers
|
|
411
|
-
memoryEventBus.emit('memory_deleted', { id });
|
|
460
|
+
memoryEventBus.emit('memory_deleted', { id, namespace: memory.namespace || 'shared' });
|
|
412
461
|
|
|
413
462
|
return text({ success: true, id, message: `Memory #${id} deleted` });
|
|
414
463
|
} catch (err) {
|
|
@@ -427,7 +476,7 @@ export function registerTools(server) {
|
|
|
427
476
|
},
|
|
428
477
|
async ({ limit, agent_id }) => {
|
|
429
478
|
try {
|
|
430
|
-
const namespace = agent_id || null;
|
|
479
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
431
480
|
const memories = getRecentMemories(limit, namespace);
|
|
432
481
|
return text({ memories, count: memories.length, namespace: namespace || 'all' });
|
|
433
482
|
} catch (err) {
|
|
@@ -446,7 +495,7 @@ export function registerTools(server) {
|
|
|
446
495
|
},
|
|
447
496
|
async ({ limit, agent_id }) => {
|
|
448
497
|
try {
|
|
449
|
-
const namespace = agent_id || null;
|
|
498
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
450
499
|
const memories = getImportantMemories(limit, namespace);
|
|
451
500
|
return text({ memories, count: memories.length, namespace: namespace || 'all' });
|
|
452
501
|
} catch (err) {
|
|
@@ -482,7 +531,7 @@ export function registerTools(server) {
|
|
|
482
531
|
source_type: 'git',
|
|
483
532
|
source_id: commit.hash,
|
|
484
533
|
confidence: 0.8
|
|
485
|
-
});
|
|
534
|
+
}, process.env.PERSYST_PROJECT || 'shared');
|
|
486
535
|
|
|
487
536
|
const embedding = await generateEmbedding(commit.fullText);
|
|
488
537
|
insertVector(id, embedding);
|
|
@@ -545,14 +594,16 @@ export function registerTools(server) {
|
|
|
545
594
|
{
|
|
546
595
|
entity_name: z.string().describe('Name of the entity'),
|
|
547
596
|
memory_id: z.number().describe('ID of the memory to link'),
|
|
548
|
-
relation: z.string().default('mentions').describe('Relationship type')
|
|
597
|
+
relation: z.string().default('mentions').describe('Relationship type'),
|
|
598
|
+
agent_id: z.string().optional().describe('Agent ID — restricts linking to this agent\'s namespace + shared')
|
|
549
599
|
},
|
|
550
|
-
async ({ entity_name, memory_id, relation }) => {
|
|
600
|
+
async ({ entity_name, memory_id, relation, agent_id }) => {
|
|
551
601
|
try {
|
|
602
|
+
const namespace = agent_id ? agent_id.toLowerCase() : null;
|
|
552
603
|
const entity = getEntityByName(entity_name);
|
|
553
604
|
if (!entity) return text({ error: `Entity "${entity_name}" not found.` });
|
|
554
605
|
|
|
555
|
-
const memory = getMemory(memory_id);
|
|
606
|
+
const memory = getMemory(memory_id, namespace);
|
|
556
607
|
if (!memory) return text({ error: `Memory #${memory_id} not found` });
|
|
557
608
|
|
|
558
609
|
insertEdge(entity.id, memory_id, relation, 'entity', 'memory');
|
|
@@ -790,8 +841,23 @@ export function registerTools(server) {
|
|
|
790
841
|
},
|
|
791
842
|
async ({ query, max_tokens, agent_id, session_id, intent }) => {
|
|
792
843
|
try {
|
|
793
|
-
const namespace = agent_id || null;
|
|
844
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
794
845
|
const contextData = await getOptimizedContext(query, max_tokens, agent_id, session_id, namespace, intent);
|
|
846
|
+
|
|
847
|
+
// Broadcast context retrieval event
|
|
848
|
+
const retrievedCount = contextData?.memories?.length ?? 0;
|
|
849
|
+
if (retrievedCount > 0) {
|
|
850
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
851
|
+
tool: 'get_optimized_context',
|
|
852
|
+
query,
|
|
853
|
+
count: retrievedCount,
|
|
854
|
+
agent_id: agent_id || 'unknown',
|
|
855
|
+
namespace: namespace || 'shared',
|
|
856
|
+
token_budget: max_tokens,
|
|
857
|
+
memory_ids: contextData.memories.map(m => m.id)
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
795
861
|
return text(contextData);
|
|
796
862
|
} catch (err) {
|
|
797
863
|
return text({ error: err.message });
|
|
@@ -830,27 +896,6 @@ function text(data) {
|
|
|
830
896
|
};
|
|
831
897
|
}
|
|
832
898
|
|
|
833
|
-
/**
|
|
834
|
-
* Compute Jaccard distance between two text strings.
|
|
835
|
-
* Used for contradiction detection — higher distance means more different content.
|
|
836
|
-
* @param {string} a - First text
|
|
837
|
-
* @param {string} b - Second text
|
|
838
|
-
* @returns {number} Distance score between 0 (identical) and 1 (completely different)
|
|
839
|
-
*/
|
|
840
|
-
function jaccardDistance(a, b) {
|
|
841
|
-
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
842
|
-
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
843
|
-
|
|
844
|
-
let intersection = 0;
|
|
845
|
-
for (const word of wordsA) {
|
|
846
|
-
if (wordsB.has(word)) intersection++;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const union = wordsA.size + wordsB.size - intersection;
|
|
850
|
-
if (union === 0) return 0;
|
|
851
|
-
return 1 - (intersection / union);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
899
|
/**
|
|
855
900
|
* Compute word-level diff between two text strings using dynamic programming.
|
|
856
901
|
* Highlights additions as [+added+] and deletions as [-deleted-].
|