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/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
- console.error(`[persyst-cache] Cache HIT for query: "${queryText.slice(0, 50)}..."`);
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.6'
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
- console.error(`[persyst] ${registeredCount} tools registered ✓`);
713
+ logInfo(`[persyst] ${registeredCount} tools registered ✓`);
684
714
 
685
- // --- Start background log watcher daemon (skip in test mode) ---
686
- if (process.env.NODE_ENV !== 'test') {
687
- startWatcher();
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
- // --- Gateway configuration ---
691
- const httpPort = parseInt(process.env.PORT || '4321', 10);
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
- if (configuredApiKey) {
696
- console.error(`[persyst] API key auth enabled — endpoints require Authorization: Bearer <key>`);
697
- }
698
- if (httpHost !== '127.0.0.1') {
699
- console.error(`[persyst] ⚠️ Gateway bound to ${httpHost} ensure PERSYST_API_KEY is set for security`);
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
- // --- Start local HTTP Gateway ---
703
- const httpServer = http.createServer((req, res) => {
704
- // CORS headers
705
- res.setHeader('Access-Control-Allow-Origin', '*');
706
- res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
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
- if (req.method === 'OPTIONS') {
710
- res.writeHead(204);
711
- res.end();
712
- return;
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
- // API key authentication middleware
716
- // /health is always public (for orchestrators / Docker health checks)
717
- if (configuredApiKey) {
718
- const urlPath = new URL(req.url || '/', 'http://127.0.0.1').pathname;
719
- if (urlPath !== '/health') {
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
- // Route GET requests (no body reading needed)
733
- if (req.method === 'GET') {
734
- try {
735
- const url = new URL(req.url || '/', `http://${httpHost}`);
736
- handleGetRequest(req, res, url).catch(err => {
737
- try {
738
- res.writeHead(500, { 'Content-Type': 'application/json' });
739
- res.end(JSON.stringify({ error: err.message }));
740
- } catch (_) {}
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
- // Route POST requests
750
- if (req.method !== 'POST') {
751
- res.writeHead(405, { 'Content-Type': 'application/json' });
752
- res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST or GET.' }));
753
- return;
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
- let body = '';
757
- req.on('data', chunk => { body += chunk; });
758
- req.on('end', async () => {
759
- try {
760
- // Handle both JSON and plain-text bodies (plain text used by /remember)
761
- const contentType = req.headers['content-type'] || '';
762
- let payload;
763
- if (contentType.includes('text/plain')) {
764
- payload = body.trim(); // Will be handled as string in /remember
765
- } else {
766
- payload = JSON.parse(body || '{}');
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
- httpServer.on('error', (err) => {
779
- if (err.code === 'EADDRINUSE') {
780
- console.error(`[persyst] HTTP Gateway port ${httpPort} already in use. Stdio MCP server will continue.`);
781
- } else {
782
- console.error('[persyst] HTTP Gateway error:', err.message);
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
- httpServer.listen(httpPort, httpHost, () => {
787
- console.error(`[persyst] HTTP Gateway listening on http://${httpHost}:${httpPort} ✓`);
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
- // --- Start temporal decay timer (every hour) ---
792
- const decayTimer = setInterval(applyTemporalDecay, 3600000);
807
+ if (req.method === 'GET') {
808
+ handleGetRequest(req, res, path, url);
809
+ return;
810
+ }
793
811
 
794
- // --- Start daily consolidation sweep ---
795
- const consolidationTimer = setInterval(async () => {
796
- console.error('[persyst] Running scheduled daily memory consolidation sweep...');
797
- try {
798
- const report = await consolidateMemories();
799
- console.error(`[persyst] Consolidation sweep: consolidated ${report.consolidated_groups} duplicate groups.`);
800
- if (report.consolidated_groups > 0) {
801
- memoryEventBus.emit('memories_consolidated', {
802
- consolidated_groups: report.consolidated_groups,
803
- details: report.details
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
- // --- SSE client health-check every 30 seconds (removes stale connections) ---
812
- const sseHealthCheck = setInterval(() => {
813
- for (const client of sseClients) {
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
- // --- Graceful shutdown ---
825
- const shutdown = () => {
826
- console.error('[persyst] Shutting down...');
827
- clearInterval(decayTimer);
828
- clearInterval(consolidationTimer);
829
- clearInterval(sseHealthCheck);
830
- stopWatcher();
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
- // Close all SSE connections gracefully
834
- for (const client of sseClients) {
835
- try {
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
- httpServer.close();
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
- // --- Connect via stdio ---
851
- const transport = new StdioServerTransport();
852
- await server.connect(transport);
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
- console.error('[persyst] MCP server running on stdio ✓');
855
- console.error('[persyst] Ready to receive tool calls');
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 namespace = (shared || !normalizedAgentId) ? 'shared' : normalizedAgentId;
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 (null = search all)
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' by default)
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: 'shared', source: 'watcher-antigravity' });
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
- // Verify against exact duplicate (Bug A fix: check namespace 'shared')
222
- if (memoryExists(fact.content, 'shared')) continue;
223
+ const watcherNs = process.env.PERSYST_PROJECT || 'shared';
224
+ if (memoryExists(fact.content, watcherNs)) continue;
223
225
 
224
- // Verify against semantic similarity (Bug B fix: check namespace 'shared')
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: 'shared', source: 'watcher-roo' });
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
- console.error('[persyst-watcher] Starting background log watcher daemon (Chokidar)...');
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
- console.error('[persyst-watcher] Background log watcher daemon stopped.');
427
+ logInfo('[persyst-watcher] Background log watcher daemon stopped.');
429
428
  }
430
429
  }