persyst-mcp 2.2.6 → 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 +94 -128
- package/bin/export.js +4 -4
- package/bin/extract.js +8 -8
- package/bin/import.js +15 -15
- package/bin/init.js +44 -33
- package/bin/mcp.js +1 -5
- package/bin/monitor.js +511 -0
- package/bin/setup.js +9 -9
- package/package.json +12 -8
- package/src/cache.js +3 -1
- package/src/database.js +4 -2
- package/src/embeddings.js +4 -2
- package/src/events.js +2 -0
- package/src/search.js +2 -2
- package/src/server.js +179 -151
- package/src/text-utils.js +11 -0
- package/src/tools.js +49 -8
- package/src/watcher.js +12 -13
package/src/events.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Events emitted:
|
|
9
9
|
* memory_added { id, content, namespace, source }
|
|
10
10
|
* memory_deleted { id }
|
|
11
|
+
* memory_updated { old_id, new_id, namespace }
|
|
12
|
+
* memory_retrieved { tool, query, count, agent_id, namespace, memory_ids, token_budget? }
|
|
11
13
|
* memories_consolidated { consolidated_groups, details }
|
|
12
14
|
*/
|
|
13
15
|
|
package/src/search.js
CHANGED
|
@@ -20,7 +20,7 @@ import db, {
|
|
|
20
20
|
import { generateEmbedding } from './embeddings.js';
|
|
21
21
|
import { createAttestation } from './attestation.js';
|
|
22
22
|
import { searchCache, LRUCache } from './cache.js';
|
|
23
|
-
import { jaccardSimilarity } from './text-utils.js';
|
|
23
|
+
import { jaccardSimilarity, logInfo } from './text-utils.js';
|
|
24
24
|
|
|
25
25
|
let lastDataVersion = 0;
|
|
26
26
|
|
|
@@ -57,7 +57,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
57
57
|
const cacheKey = LRUCache.key(`${ns}:${queryText}`, parsedLimit);
|
|
58
58
|
const cached = searchCache.get(cacheKey);
|
|
59
59
|
if (cached) {
|
|
60
|
-
|
|
60
|
+
logInfo(`[persyst-cache] Cache HIT for query: "${queryText.slice(0, 50)}..."`);
|
|
61
61
|
return cached;
|
|
62
62
|
}
|
|
63
63
|
|
package/src/server.js
CHANGED
|
@@ -35,6 +35,7 @@ import { consolidateMemories, searchHybrid, getOptimizedContext } from './search
|
|
|
35
35
|
import { startWatcher, stopWatcher } from './watcher.js';
|
|
36
36
|
import { verifyChainIntegrity } from './attestation.js';
|
|
37
37
|
import { memoryEventBus } from './events.js';
|
|
38
|
+
import { logInfo } from './text-utils.js';
|
|
38
39
|
|
|
39
40
|
// Track server birth time for uptime reporting
|
|
40
41
|
const SERVER_START_TIME = Date.now();
|
|
@@ -348,7 +349,7 @@ async function handleGetRequest(req, res, url) {
|
|
|
348
349
|
res.write(`event: connected\ndata: ${JSON.stringify({
|
|
349
350
|
ok: true,
|
|
350
351
|
timestamp: new Date().toISOString(),
|
|
351
|
-
server_version: '2.2.
|
|
352
|
+
server_version: '2.2.7'
|
|
352
353
|
})}\n\n`);
|
|
353
354
|
|
|
354
355
|
sseClients.add(res);
|
|
@@ -364,18 +365,28 @@ async function handleGetRequest(req, res, url) {
|
|
|
364
365
|
const onDeleted = (data) => {
|
|
365
366
|
try { res.write(`event: memory_deleted\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
366
367
|
};
|
|
368
|
+
const onUpdated = (data) => {
|
|
369
|
+
try { res.write(`event: memory_updated\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
370
|
+
};
|
|
371
|
+
const onRetrieved = (data) => {
|
|
372
|
+
try { res.write(`event: memory_retrieved\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
373
|
+
};
|
|
367
374
|
const onConsolidated = (data) => {
|
|
368
375
|
try { res.write(`event: memories_consolidated\ndata: ${JSON.stringify(data)}\n\n`); } catch (_) {}
|
|
369
376
|
};
|
|
370
377
|
|
|
371
378
|
memoryEventBus.on('memory_added', onAdded);
|
|
372
379
|
memoryEventBus.on('memory_deleted', onDeleted);
|
|
380
|
+
memoryEventBus.on('memory_updated', onUpdated);
|
|
381
|
+
memoryEventBus.on('memory_retrieved', onRetrieved);
|
|
373
382
|
memoryEventBus.on('memories_consolidated', onConsolidated);
|
|
374
383
|
|
|
375
384
|
req.on('close', () => {
|
|
376
385
|
clearInterval(heartbeat);
|
|
377
386
|
memoryEventBus.off('memory_added', onAdded);
|
|
378
387
|
memoryEventBus.off('memory_deleted', onDeleted);
|
|
388
|
+
memoryEventBus.off('memory_updated', onUpdated);
|
|
389
|
+
memoryEventBus.off('memory_retrieved', onRetrieved);
|
|
379
390
|
memoryEventBus.off('memories_consolidated', onConsolidated);
|
|
380
391
|
sseClients.delete(res);
|
|
381
392
|
console.error(`[persyst-sse] Client disconnected. Active: ${sseClients.size}`);
|
|
@@ -463,6 +474,16 @@ async function handlePostRequest(req, res, payload) {
|
|
|
463
474
|
return;
|
|
464
475
|
}
|
|
465
476
|
const results = await searchHybrid(query, limit, agent_id, session_id, agent_id || null);
|
|
477
|
+
if (results && results.length > 0) {
|
|
478
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
479
|
+
tool: 'http/search',
|
|
480
|
+
query,
|
|
481
|
+
count: results.length,
|
|
482
|
+
agent_id: agent_id || 'http',
|
|
483
|
+
namespace: agent_id || 'shared',
|
|
484
|
+
memory_ids: results.map(r => r.id)
|
|
485
|
+
});
|
|
486
|
+
}
|
|
466
487
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
467
488
|
res.end(JSON.stringify({ success: true, results }));
|
|
468
489
|
return;
|
|
@@ -506,6 +527,18 @@ async function handlePostRequest(req, res, payload) {
|
|
|
506
527
|
return;
|
|
507
528
|
}
|
|
508
529
|
const context = await getOptimizedContext(query, max_tokens, agent_id, session_id, agent_id || null, intent);
|
|
530
|
+
const retrievedCount = context?.memories?.length ?? 0;
|
|
531
|
+
if (retrievedCount > 0) {
|
|
532
|
+
memoryEventBus.emit('memory_retrieved', {
|
|
533
|
+
tool: 'http/context',
|
|
534
|
+
query,
|
|
535
|
+
count: retrievedCount,
|
|
536
|
+
agent_id: agent_id || 'http',
|
|
537
|
+
namespace: agent_id || 'shared',
|
|
538
|
+
token_budget: max_tokens,
|
|
539
|
+
memory_ids: context.memories.map(m => m.id)
|
|
540
|
+
});
|
|
541
|
+
}
|
|
509
542
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
510
543
|
res.end(JSON.stringify(context));
|
|
511
544
|
return;
|
|
@@ -668,9 +701,6 @@ async function handlePostRequest(req, res, payload) {
|
|
|
668
701
|
// MAIN SERVER STARTUP
|
|
669
702
|
// ============================================================
|
|
670
703
|
|
|
671
|
-
/**
|
|
672
|
-
* Start the Persyst MCP server & HTTP Gateway.
|
|
673
|
-
*/
|
|
674
704
|
export async function startServer() {
|
|
675
705
|
// --- Create MCP server ---
|
|
676
706
|
const server = new McpServer({
|
|
@@ -680,177 +710,175 @@ export async function startServer() {
|
|
|
680
710
|
|
|
681
711
|
// --- Register all tools ---
|
|
682
712
|
const registeredCount = registerTools(server);
|
|
683
|
-
|
|
713
|
+
logInfo(`[persyst] ${registeredCount} tools registered ✓`);
|
|
684
714
|
|
|
685
|
-
// ---
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
715
|
+
// --- Connect via stdio IMMEDIATELY so MCP handshake completes instantly (<10ms) ---
|
|
716
|
+
const transport = new StdioServerTransport();
|
|
717
|
+
await server.connect(transport);
|
|
689
718
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const httpHost = process.env.PERSYST_HOST || '127.0.0.1';
|
|
693
|
-
const configuredApiKey = process.env.PERSYST_API_KEY || null;
|
|
719
|
+
logInfo('[persyst] MCP server running on stdio ✓');
|
|
720
|
+
logInfo('[persyst] Ready to receive tool calls');
|
|
694
721
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
console.error(`[
|
|
722
|
+
// Interactive Terminal Banner (only shown when run directly by a user in terminal)
|
|
723
|
+
if (process.stderr.isTTY || process.stdout.isTTY) {
|
|
724
|
+
console.error(`\n[OK] Persyst MCP Server is active and listening (stdio mode)`);
|
|
725
|
+
console.error(`[OK] Workspace Project: ${process.env.PERSYST_PROJECT || 'shared'}`);
|
|
726
|
+
console.error(`[OK] Local HTTP Gateway: http://127.0.0.1:${process.env.PORT || '4321'}`);
|
|
727
|
+
console.error(`[OK] Process ID: ${process.pid} | Press Ctrl+C to stop.\n`);
|
|
700
728
|
}
|
|
701
729
|
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
730
|
+
// Defer background services & HTTP server so stdio handshake is never blocked
|
|
731
|
+
let httpServer = null;
|
|
732
|
+
let decayTimer = null;
|
|
733
|
+
let consolidationTimer = null;
|
|
734
|
+
let sseHealthCheck = null;
|
|
708
735
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
736
|
+
const shutdown = () => {
|
|
737
|
+
logInfo('[persyst] Shutting down...');
|
|
738
|
+
if (decayTimer) clearInterval(decayTimer);
|
|
739
|
+
if (consolidationTimer) clearInterval(consolidationTimer);
|
|
740
|
+
if (sseHealthCheck) clearInterval(sseHealthCheck);
|
|
741
|
+
stopWatcher();
|
|
742
|
+
cleanupWatchers();
|
|
714
743
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const authHeader = req.headers['authorization'] || '';
|
|
721
|
-
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
722
|
-
if (token !== configuredApiKey) {
|
|
723
|
-
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
724
|
-
res.end(JSON.stringify({
|
|
725
|
-
error: 'Unauthorized. Set header: Authorization: Bearer <PERSYST_API_KEY>'
|
|
726
|
-
}));
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
744
|
+
for (const client of sseClients) {
|
|
745
|
+
try {
|
|
746
|
+
client.write(`event: server_shutdown\ndata: ${JSON.stringify({ message: 'Server shutting down' })}\n\n`);
|
|
747
|
+
client.end();
|
|
748
|
+
} catch (_) {}
|
|
730
749
|
}
|
|
750
|
+
sseClients.clear();
|
|
731
751
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
} catch (err) {
|
|
743
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
744
|
-
res.end(JSON.stringify({ error: 'Bad request URL' }));
|
|
745
|
-
}
|
|
746
|
-
return;
|
|
752
|
+
if (httpServer) httpServer.close();
|
|
753
|
+
closeDatabase();
|
|
754
|
+
};
|
|
755
|
+
process.on('SIGINT', shutdown);
|
|
756
|
+
process.on('SIGTERM', shutdown);
|
|
757
|
+
|
|
758
|
+
setTimeout(() => {
|
|
759
|
+
// --- Start background log watcher daemon (skip in test mode) ---
|
|
760
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
761
|
+
startWatcher();
|
|
747
762
|
}
|
|
748
763
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
764
|
+
// --- Gateway configuration ---
|
|
765
|
+
const httpPort = parseInt(process.env.PORT || '4321', 10);
|
|
766
|
+
const httpHost = process.env.PERSYST_HOST || '127.0.0.1';
|
|
767
|
+
const configuredApiKey = process.env.PERSYST_API_KEY || null;
|
|
768
|
+
|
|
769
|
+
if (configuredApiKey) {
|
|
770
|
+
logInfo(`[persyst] API key auth enabled — endpoints require Authorization: Bearer <key>`);
|
|
771
|
+
}
|
|
772
|
+
if (httpHost !== '127.0.0.1') {
|
|
773
|
+
logInfo(`[persyst] ⚠️ Gateway bound to ${httpHost} — ensure PERSYST_API_KEY is set for security`);
|
|
754
774
|
}
|
|
755
775
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
await handlePostRequest(req, res, payload);
|
|
769
|
-
} catch (err) {
|
|
770
|
-
try {
|
|
771
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
772
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
773
|
-
} catch (_) {}
|
|
776
|
+
// --- Start local HTTP Gateway ---
|
|
777
|
+
httpServer = http.createServer((req, res) => {
|
|
778
|
+
// CORS headers
|
|
779
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
780
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
781
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
782
|
+
|
|
783
|
+
if (req.method === 'OPTIONS') {
|
|
784
|
+
res.writeHead(204);
|
|
785
|
+
res.end();
|
|
786
|
+
return;
|
|
774
787
|
}
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
788
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
789
|
+
if (configuredApiKey) {
|
|
790
|
+
const urlPath = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
791
|
+
if (urlPath !== '/health') {
|
|
792
|
+
const authHeader = req.headers['authorization'] || '';
|
|
793
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
794
|
+
if (token !== configuredApiKey) {
|
|
795
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
796
|
+
res.end(JSON.stringify({
|
|
797
|
+
error: 'Unauthorized. Set header: Authorization: Bearer <PERSYST_API_KEY>'
|
|
798
|
+
}));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
785
803
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
console.error(`[persyst] Endpoints: /health /stats /system-prompt /events /remember /search /add /context /tool /verify /batch/add /batch/search`);
|
|
789
|
-
});
|
|
804
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
805
|
+
const path = url.pathname;
|
|
790
806
|
|
|
791
|
-
|
|
792
|
-
|
|
807
|
+
if (req.method === 'GET') {
|
|
808
|
+
handleGetRequest(req, res, path, url);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
793
811
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
812
|
+
if (req.method === 'POST') {
|
|
813
|
+
let body = '';
|
|
814
|
+
req.on('data', chunk => {
|
|
815
|
+
body += chunk;
|
|
816
|
+
if (body.length > 10 * 1024 * 1024) {
|
|
817
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
818
|
+
res.end(JSON.stringify({ error: 'Payload too large. Max 10MB.' }));
|
|
819
|
+
req.destroy();
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
req.on('end', () => {
|
|
823
|
+
try {
|
|
824
|
+
const payload = body ? JSON.parse(body) : {};
|
|
825
|
+
handlePostRequest(req, res, payload).catch(err => {
|
|
826
|
+
try {
|
|
827
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
828
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
829
|
+
} catch (_) {}
|
|
830
|
+
});
|
|
831
|
+
} catch (err) {
|
|
832
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
833
|
+
res.end(JSON.stringify({ error: `Invalid JSON payload: ${err.message}` }));
|
|
834
|
+
}
|
|
804
835
|
});
|
|
836
|
+
return;
|
|
805
837
|
}
|
|
806
|
-
} catch (err) {
|
|
807
|
-
console.error('[persyst] Daily consolidation sweep failed:', err.message);
|
|
808
|
-
}
|
|
809
|
-
}, 86400000);
|
|
810
838
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
try {
|
|
815
|
-
client.write(': health-check\n\n');
|
|
816
|
-
} catch (_) {
|
|
817
|
-
// Client is stale — remove it
|
|
818
|
-
try { client.end(); } catch (_) {}
|
|
819
|
-
sseClients.delete(client);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}, 30000);
|
|
839
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
840
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
841
|
+
});
|
|
823
842
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
cleanupWatchers();
|
|
843
|
+
httpServer.on('error', (err) => {
|
|
844
|
+
if (err.code === 'EADDRINUSE') {
|
|
845
|
+
logInfo(`[persyst] HTTP Gateway port ${httpPort} already in use. Stdio MCP server will continue.`);
|
|
846
|
+
} else {
|
|
847
|
+
console.error('[persyst] HTTP Gateway error:', err.message);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
832
850
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
client.write(`event: server_shutdown\ndata: ${JSON.stringify({ message: 'Server shutting down' })}\n\n`);
|
|
837
|
-
client.end();
|
|
838
|
-
} catch (_) {}
|
|
839
|
-
}
|
|
840
|
-
sseClients.clear();
|
|
851
|
+
httpServer.listen(httpPort, httpHost, () => {
|
|
852
|
+
logInfo(`[persyst] HTTP Gateway listening on http://${httpHost}:${httpPort} ✓`);
|
|
853
|
+
});
|
|
841
854
|
|
|
842
|
-
|
|
843
|
-
closeDatabase();
|
|
844
|
-
// Let the process exit naturally after all handles are closed
|
|
845
|
-
// process.exit(0) removed — Node exits on its own when event loop is empty
|
|
846
|
-
};
|
|
847
|
-
process.on('SIGINT', shutdown);
|
|
848
|
-
process.on('SIGTERM', shutdown);
|
|
855
|
+
decayTimer = setInterval(applyTemporalDecay, 3600000);
|
|
849
856
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
857
|
+
consolidationTimer = setInterval(async () => {
|
|
858
|
+
logInfo('[persyst] Running scheduled daily memory consolidation sweep...');
|
|
859
|
+
try {
|
|
860
|
+
const report = await consolidateMemories();
|
|
861
|
+
logInfo(`[persyst] Consolidation sweep: consolidated ${report.consolidated_groups} duplicate groups.`);
|
|
862
|
+
if (report.consolidated_groups > 0) {
|
|
863
|
+
memoryEventBus.emit('memories_consolidated', {
|
|
864
|
+
consolidated_groups: report.consolidated_groups,
|
|
865
|
+
details: report.details
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.error('[persyst] Daily consolidation sweep failed:', err.message);
|
|
870
|
+
}
|
|
871
|
+
}, 86400000);
|
|
853
872
|
|
|
854
|
-
|
|
855
|
-
|
|
873
|
+
sseHealthCheck = setInterval(() => {
|
|
874
|
+
for (const client of sseClients) {
|
|
875
|
+
try {
|
|
876
|
+
client.write(': health-check\n\n');
|
|
877
|
+
} catch (_) {
|
|
878
|
+
try { client.end(); } catch (_) {}
|
|
879
|
+
sseClients.delete(client);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}, 30000);
|
|
883
|
+
}, 50);
|
|
856
884
|
}
|
package/src/text-utils.js
CHANGED
|
@@ -39,3 +39,14 @@ export function jaccardSimilarity(a, b) {
|
|
|
39
39
|
export function jaccardDistance(a, b) {
|
|
40
40
|
return 1 - jaccardSimilarity(a, b);
|
|
41
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
|
@@ -121,8 +121,10 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
|
|
|
121
121
|
return { error: validation.error };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// Derive namespace from agent_id and shared flag
|
|
125
|
-
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);
|
|
126
128
|
|
|
127
129
|
// Deduplication check (namespace-aware)
|
|
128
130
|
const existing = getMemoryByContent(redactedContent, namespace);
|
|
@@ -309,9 +311,22 @@ export function registerTools(server) {
|
|
|
309
311
|
},
|
|
310
312
|
async ({ query, limit, agent_id, session_id }) => {
|
|
311
313
|
try {
|
|
312
|
-
// Derive namespace from agent_id
|
|
313
|
-
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;
|
|
314
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
|
+
|
|
315
330
|
return text({
|
|
316
331
|
results,
|
|
317
332
|
count: results.length,
|
|
@@ -337,6 +352,17 @@ export function registerTools(server) {
|
|
|
337
352
|
const namespace = agent_id ? agent_id.toLowerCase() : null;
|
|
338
353
|
const memory = getMemory(id, namespace);
|
|
339
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
|
+
|
|
340
366
|
return text(memory);
|
|
341
367
|
} catch (err) {
|
|
342
368
|
return text({ error: err.message });
|
|
@@ -450,7 +476,7 @@ export function registerTools(server) {
|
|
|
450
476
|
},
|
|
451
477
|
async ({ limit, agent_id }) => {
|
|
452
478
|
try {
|
|
453
|
-
const namespace = agent_id || null;
|
|
479
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
454
480
|
const memories = getRecentMemories(limit, namespace);
|
|
455
481
|
return text({ memories, count: memories.length, namespace: namespace || 'all' });
|
|
456
482
|
} catch (err) {
|
|
@@ -469,7 +495,7 @@ export function registerTools(server) {
|
|
|
469
495
|
},
|
|
470
496
|
async ({ limit, agent_id }) => {
|
|
471
497
|
try {
|
|
472
|
-
const namespace = agent_id || null;
|
|
498
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
473
499
|
const memories = getImportantMemories(limit, namespace);
|
|
474
500
|
return text({ memories, count: memories.length, namespace: namespace || 'all' });
|
|
475
501
|
} catch (err) {
|
|
@@ -505,7 +531,7 @@ export function registerTools(server) {
|
|
|
505
531
|
source_type: 'git',
|
|
506
532
|
source_id: commit.hash,
|
|
507
533
|
confidence: 0.8
|
|
508
|
-
});
|
|
534
|
+
}, process.env.PERSYST_PROJECT || 'shared');
|
|
509
535
|
|
|
510
536
|
const embedding = await generateEmbedding(commit.fullText);
|
|
511
537
|
insertVector(id, embedding);
|
|
@@ -815,8 +841,23 @@ export function registerTools(server) {
|
|
|
815
841
|
},
|
|
816
842
|
async ({ query, max_tokens, agent_id, session_id, intent }) => {
|
|
817
843
|
try {
|
|
818
|
-
const namespace = agent_id || null;
|
|
844
|
+
const namespace = agent_id || process.env.PERSYST_PROJECT || null;
|
|
819
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
|
+
|
|
820
861
|
return text(contextData);
|
|
821
862
|
} catch (err) {
|
|
822
863
|
return text({ error: err.message });
|
package/src/watcher.js
CHANGED
|
@@ -22,6 +22,7 @@ import { extractHeuristic, hasExtractableSignals } from './extractor-heuristic.j
|
|
|
22
22
|
import { searchHybrid } from './search.js';
|
|
23
23
|
import { searchCache } from './cache.js';
|
|
24
24
|
import { memoryEventBus } from './events.js';
|
|
25
|
+
import { logInfo } from './text-utils.js';
|
|
25
26
|
import chokidar from 'chokidar';
|
|
26
27
|
|
|
27
28
|
// Config path: ~/.persyst/config.json (overridable for tests)
|
|
@@ -150,12 +151,13 @@ async function processJsonlFile(filePath) {
|
|
|
150
151
|
continue;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
// Insert memory with provenance (written to 'shared'
|
|
154
|
+
// Insert memory with provenance (written to project namespace or 'shared')
|
|
155
|
+
const watcherNs = process.env.PERSYST_PROJECT || 'shared';
|
|
154
156
|
const id = insertMemory(fact.content, fact.confidence, {
|
|
155
157
|
source_type: 'agent',
|
|
156
158
|
source_id: record.source === 'MODEL' ? 'antigravity-worker' : 'user-dialogue',
|
|
157
159
|
confidence: fact.confidence
|
|
158
|
-
});
|
|
160
|
+
}, watcherNs);
|
|
159
161
|
|
|
160
162
|
try {
|
|
161
163
|
const embedding = await generateEmbedding(fact.content);
|
|
@@ -169,7 +171,7 @@ async function processJsonlFile(filePath) {
|
|
|
169
171
|
|
|
170
172
|
addedCount++;
|
|
171
173
|
console.error(`[persyst-watcher] Auto-extracted fact: "${fact.content}" (Memory #${id})`);
|
|
172
|
-
memoryEventBus.emit('memory_added', { id, content: fact.content, namespace:
|
|
174
|
+
memoryEventBus.emit('memory_added', { id, content: fact.content, namespace: watcherNs, source: 'watcher-antigravity' });
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
}
|
|
@@ -218,35 +220,32 @@ async function processJsonFile(filePath) {
|
|
|
218
220
|
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
219
221
|
const facts = extractHeuristic(msg.content);
|
|
220
222
|
for (const fact of facts) {
|
|
221
|
-
|
|
222
|
-
if (memoryExists(fact.content,
|
|
223
|
+
const watcherNs = process.env.PERSYST_PROJECT || 'shared';
|
|
224
|
+
if (memoryExists(fact.content, watcherNs)) continue;
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
const similar = await searchHybrid(fact.content, 1, null, null, 'shared');
|
|
226
|
+
const similar = await searchHybrid(fact.content, 1, null, null, watcherNs);
|
|
226
227
|
if (similar.length > 0 && parseFloat(similar[0].similarity) >= DEDUP_THRESHOLD) {
|
|
227
228
|
continue;
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
// Insert memory with provenance (written to 'shared' by default)
|
|
231
231
|
const id = insertMemory(fact.content, fact.confidence, {
|
|
232
232
|
source_type: 'agent',
|
|
233
233
|
source_id: msg.role === 'assistant' ? 'roo-worker' : 'user-dialogue',
|
|
234
234
|
confidence: fact.confidence
|
|
235
|
-
});
|
|
235
|
+
}, watcherNs);
|
|
236
236
|
|
|
237
237
|
try {
|
|
238
238
|
const embedding = await generateEmbedding(fact.content);
|
|
239
239
|
insertVector(id, embedding);
|
|
240
240
|
} catch (embedErr) {
|
|
241
241
|
console.error(`[persyst-watcher] Embedding failed for fact #${id}: ${embedErr.message}`);
|
|
242
|
-
// Clean up: delete the memory so we don't have orphaned entries
|
|
243
242
|
try { deleteMemory(id); } catch (_) {}
|
|
244
243
|
continue;
|
|
245
244
|
}
|
|
246
245
|
|
|
247
246
|
addedCount++;
|
|
248
247
|
console.error(`[persyst-watcher] Auto-extracted fact: "${fact.content}" (Memory #${id})`);
|
|
249
|
-
memoryEventBus.emit('memory_added', { id, content: fact.content, namespace:
|
|
248
|
+
memoryEventBus.emit('memory_added', { id, content: fact.content, namespace: watcherNs, source: 'watcher-roo' });
|
|
250
249
|
}
|
|
251
250
|
}
|
|
252
251
|
}
|
|
@@ -381,7 +380,7 @@ async function handleFileChange(filePath) {
|
|
|
381
380
|
export function startWatcher() {
|
|
382
381
|
if (chokidarWatcher) return;
|
|
383
382
|
|
|
384
|
-
|
|
383
|
+
logInfo('[persyst-watcher] Starting background log watcher daemon (Chokidar)...');
|
|
385
384
|
const watchDirs = loadWatchedDirs();
|
|
386
385
|
|
|
387
386
|
// Run initial scan, then start watching
|
|
@@ -425,6 +424,6 @@ export function stopWatcher() {
|
|
|
425
424
|
if (chokidarWatcher) {
|
|
426
425
|
chokidarWatcher.close().catch(() => {});
|
|
427
426
|
chokidarWatcher = null;
|
|
428
|
-
|
|
427
|
+
logInfo('[persyst-watcher] Background log watcher daemon stopped.');
|
|
429
428
|
}
|
|
430
429
|
}
|