persyst-mcp 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/database.js CHANGED
@@ -77,6 +77,11 @@ try {
77
77
  db.exec("ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'shared'");
78
78
  } catch (e) { /* Column already exists */ }
79
79
 
80
+ // --- Migration: add parent_id column for history tracing ---
81
+ try {
82
+ db.exec('ALTER TABLE memories ADD COLUMN parent_id INTEGER DEFAULT NULL');
83
+ } catch (e) { /* Column already exists */ }
84
+
80
85
  // --- Index on namespace for fast filtered queries ---
81
86
  try {
82
87
  db.exec('CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories (namespace)');
@@ -226,7 +231,7 @@ console.error('[persyst] Schema initialized ✓');
226
231
  const stmts = {
227
232
  // -- Insert --
228
233
  insertMemory: db.prepare(
229
- 'INSERT INTO memories (content, importance_score, namespace) VALUES (?, ?, ?)'
234
+ 'INSERT INTO memories (content, importance_score, namespace, parent_id) VALUES (?, ?, ?, ?)'
230
235
  ),
231
236
  insertVec: db.prepare(
232
237
  'INSERT INTO memories_vec (rowid, embedding) VALUES (?, ?)'
@@ -283,7 +288,7 @@ const stmts = {
283
288
  "SELECT * FROM memories WHERE (namespace = ? OR namespace = 'shared') AND valid_until IS NULL ORDER BY importance_score DESC LIMIT ?"
284
289
  ),
285
290
  getProvenance: db.prepare(
286
- 'SELECT * FROM provenance WHERE memory_id = ?'
291
+ 'SELECT * FROM provenance WHERE memory_id = ? ORDER BY id DESC'
287
292
  ),
288
293
  getAllAgentStats: db.prepare(
289
294
  'SELECT * FROM agent_stats ORDER BY reputation_score DESC'
@@ -434,13 +439,19 @@ const stmts = {
434
439
  * @param {string} namespace - Namespace for agent isolation (default: 'shared')
435
440
  * @returns {number} The new memory's ID
436
441
  */
437
- export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared') {
438
- const result = stmts.insertMemory.run(content, importance, namespace || 'shared');
442
+ export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared', parentId = null) {
443
+ if (content && content.length > 10000) {
444
+ throw new Error('Memory content exceeds maximum length of 10000 characters.');
445
+ }
446
+ const result = stmts.insertMemory.run(content, importance, namespace || 'shared', parentId);
439
447
  const id = Number(result.lastInsertRowid);
440
448
 
441
449
  // Provenance Info handling
442
450
  const source_type = provenanceInfo?.source_type || 'manual';
443
- const source_id = provenanceInfo?.source_id || null;
451
+ let source_id = provenanceInfo?.source_id || null;
452
+ if (source_type === 'agent' && source_id) {
453
+ source_id = source_id.toLowerCase();
454
+ }
444
455
  const confidence = provenanceInfo?.confidence !== undefined ? provenanceInfo.confidence : 1.0;
445
456
 
446
457
  stmts.insertProvenance.run(id, source_type, source_id, confidence);
@@ -533,6 +544,12 @@ export function deleteVec(id) {
533
544
  export function deleteMemory(id) {
534
545
  stmts.deleteEdgesByMemory.run(id, id);
535
546
  deleteVec(id); // Remove vector first (no cascades on virtual tables)
547
+ try {
548
+ db.prepare('DELETE FROM provenance WHERE memory_id = ?').run(id);
549
+ db.prepare('DELETE FROM contradictions WHERE old_memory_id = ? OR new_memory_id = ?').run(id, id);
550
+ } catch (e) {
551
+ console.error(`[persyst] Clean up provenance/contradictions error: ${e.message}`);
552
+ }
536
553
  const result = stmts.deleteMemory.run(id);
537
554
  return result.changes > 0;
538
555
  }
@@ -758,14 +775,23 @@ export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
758
775
  stmts.archiveMemory.run(oldMemoryId);
759
776
  stmts.insertContradiction.run(oldMemoryId, newMemoryId, reason);
760
777
 
778
+ // Set parent_id to link memories for bidirectional history tracing (always newer pointing to older)
779
+ try {
780
+ const parentId = Math.min(oldMemoryId, newMemoryId);
781
+ const childId = Math.max(oldMemoryId, newMemoryId);
782
+ db.prepare('UPDATE memories SET parent_id = ? WHERE id = ?').run(parentId, childId);
783
+ } catch (e) {
784
+ console.error(`[persyst] Failed to set parent_id on contradiction: ${e.message}`);
785
+ }
786
+
761
787
  // Retrieve provenance of both versions for game-theoretic reputation calculation
762
788
  const oldProvenance = getProvenance(oldMemoryId);
763
789
  const newProvenance = getProvenance(newMemoryId);
764
790
 
765
791
  if (oldProvenance && oldProvenance.source_type === 'agent' && oldProvenance.source_id) {
766
- const isSelfCorrection = newProvenance &&
767
- newProvenance.source_type === 'agent' &&
768
- newProvenance.source_id === oldProvenance.source_id;
792
+ const isSelfCorrection = (newProvenance && newProvenance.source_id &&
793
+ newProvenance.source_id.toLowerCase() === oldProvenance.source_id.toLowerCase()) ||
794
+ reason.includes('update_memory');
769
795
  if (!isSelfCorrection) {
770
796
  // Different agent/manual source contradicts the old memory
771
797
  incrementAgentStat(oldProvenance.source_id, 'contradicted');
@@ -782,22 +808,30 @@ export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
782
808
  * Get provenance for a memory.
783
809
  */
784
810
  export function getProvenance(memoryId) {
785
- return stmts.getProvenance.get(memoryId) || null;
811
+ const prov = stmts.getProvenance.get(memoryId) || null;
812
+ if (prov && prov.source_type === 'agent' && prov.source_id) {
813
+ prov.source_id = prov.source_id.toLowerCase();
814
+ }
815
+ return prov;
786
816
  }
787
817
 
788
818
  /**
789
819
  * Update agent reputation counters.
790
820
  */
791
821
  export function incrementAgentStat(agentId, action) {
792
- stmts.upsertAgent.run(agentId);
822
+ const normalizedAgentId = agentId.toLowerCase();
823
+ if (normalizedAgentId === 'antigravity-worker' || normalizedAgentId === 'user-dialogue') {
824
+ return; // Ignore internal/system identities from reputation penalties
825
+ }
826
+ stmts.upsertAgent.run(normalizedAgentId);
793
827
  if (action === 'created') {
794
- stmts.incrementCreated.run(agentId);
828
+ stmts.incrementCreated.run(normalizedAgentId);
795
829
  } else if (action === 'confirmed') {
796
- stmts.incrementConfirmed.run(agentId);
830
+ stmts.incrementConfirmed.run(normalizedAgentId);
797
831
  } else if (action === 'contradicted') {
798
- stmts.incrementContradicted.run(agentId);
832
+ stmts.incrementContradicted.run(normalizedAgentId);
799
833
  }
800
- stmts.recalculateReputation.run(agentId);
834
+ stmts.recalculateReputation.run(normalizedAgentId);
801
835
  }
802
836
 
803
837
  /**
@@ -857,13 +891,25 @@ export function getMemoryHistoryChain(memoryId) {
857
891
  if (versions.has(currentId)) continue;
858
892
  versions.add(currentId);
859
893
 
860
- // Find ancestors (replaced by current) using prepared statement
894
+ // 1. Find parent (ancestor) from memories table
895
+ const row = db.prepare('SELECT parent_id FROM memories WHERE id = ?').get(currentId);
896
+ if (row && row.parent_id !== null) {
897
+ if (!versions.has(row.parent_id)) queue.push(row.parent_id);
898
+ }
899
+
900
+ // 2. Find children (descendants) from memories table
901
+ const children = db.prepare('SELECT id FROM memories WHERE parent_id = ?').all(currentId);
902
+ for (const child of children) {
903
+ if (!versions.has(child.id)) queue.push(child.id);
904
+ }
905
+
906
+ // 3. Fallback: Find ancestors (replaced by current) from contradictions table
861
907
  const ancestors = stmts.getContradictionAncestors.all(currentId);
862
908
  ancestors.forEach(a => {
863
909
  if (!versions.has(a.old_memory_id)) queue.push(a.old_memory_id);
864
910
  });
865
911
 
866
- // Find descendants (replaces current) using prepared statement
912
+ // 4. Fallback: Find descendants (replaces current) from contradictions table
867
913
  const descendants = stmts.getContradictionDescendants.all(currentId);
868
914
  descendants.forEach(d => {
869
915
  if (!versions.has(d.new_memory_id)) queue.push(d.new_memory_id);
@@ -875,13 +921,27 @@ export function getMemoryHistoryChain(memoryId) {
875
921
 
876
922
  const placeholders = ids.map(() => '?').join(',');
877
923
  const rows = db.prepare(`
878
- SELECT m.*, p.source_type, p.source_id, p.confidence
879
- FROM memories m
880
- LEFT JOIN provenance p ON m.id = p.memory_id
881
- WHERE m.id IN (${placeholders})
882
- ORDER BY m.created_at ASC
924
+ SELECT * FROM memories
925
+ WHERE id IN (${placeholders})
926
+ ORDER BY created_at ASC
883
927
  `).all(...ids);
884
928
 
929
+ for (const row of rows) {
930
+ const prov = getProvenance(row.id);
931
+ if (prov) {
932
+ row.source_type = prov.source_type;
933
+ row.source_id = prov.source_id;
934
+ row.confidence = prov.confidence;
935
+ } else {
936
+ row.source_type = 'manual';
937
+ row.source_id = null;
938
+ row.confidence = 1.0;
939
+ }
940
+ if (row.source_type === 'agent' && row.source_id) {
941
+ row.source_id = row.source_id.toLowerCase();
942
+ }
943
+ }
944
+
885
945
  return rows;
886
946
  }
887
947
 
package/src/search.js CHANGED
@@ -13,7 +13,8 @@ import db, {
13
13
  getMemoryById,
14
14
  boostMemory,
15
15
  getProvenance,
16
- getMemoriesByEntity
16
+ getMemoriesByEntity,
17
+ getAllEntities
17
18
  } from './database.js';
18
19
  import { generateEmbedding } from './embeddings.js';
19
20
  import { createAttestation } from './attestation.js';
@@ -31,7 +32,7 @@ let lastDataVersion = 0;
31
32
  * @param {string|null} sessionId - Session identifier
32
33
  * @returns {Promise<Array>} Ranked search results (with .attestation property attached)
33
34
  */
34
- export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null) {
35
+ export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null, skipAttestation = false) {
35
36
  // Sync in-memory cache with external DB changes using sqlite data_version
36
37
  try {
37
38
  const currentDataVersion = db.pragma('data_version', { simple: true });
@@ -142,11 +143,12 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
142
143
  // --- Step 5: Apply MMR for diverse retrieval (Feature 3) ---
143
144
  const mmrResults = applyMMR(finalResults, limit);
144
145
 
145
- // Generate cryptographic attestation for audit trails
146
- const attestation = createAttestation(queryText, mmrResults, agentId, sessionId);
147
-
148
- // Attach attestation object directly to the array to preserve compatibility with existing tests
149
- mmrResults.attestation = attestation;
146
+ // Generate cryptographic attestation for audit trails (skip if called internally)
147
+ let attestation = null;
148
+ if (!skipAttestation) {
149
+ attestation = createAttestation(queryText, mmrResults, agentId, sessionId);
150
+ mmrResults.attestation = attestation;
151
+ }
150
152
 
151
153
  // --- Store in LRU cache (Feature 1) ---
152
154
  searchCache.set(cacheKey, mmrResults);
@@ -239,8 +241,18 @@ function jaccardSimilarity(a, b) {
239
241
  * @param {string|null} sessionId - Current session ID
240
242
  */
241
243
  export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null) {
242
- // 1. Run hybrid search to fetch top 20 memories (namespace-aware)
243
- const searchHits = await searchHybrid(queryText, 20, agentId, sessionId, namespace);
244
+ // Extract entities mentioned in the query text to seed the graph search directly
245
+ const entities = getAllEntities(100);
246
+ const matchedEntityIds = new Set();
247
+ for (const ent of entities) {
248
+ const entNameLower = ent.name.toLowerCase();
249
+ if (queryText.toLowerCase().includes(entNameLower)) {
250
+ matchedEntityIds.add(ent.id);
251
+ }
252
+ }
253
+
254
+ // 1. Run hybrid search to fetch top 5 memories as seeds (skip attestation to avoid double-write)
255
+ const searchHits = await searchHybrid(queryText, 5, agentId, sessionId, namespace, true);
244
256
  const candidates = new Map();
245
257
 
246
258
  for (const hit of searchHits) {
@@ -254,63 +266,117 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
254
266
  provenance: hit.provenance,
255
267
  source: 'search'
256
268
  });
269
+ }
270
+
271
+ // 2. Perform Graph Hop (multi-hop traversal) globally
272
+ const hopQueue = [];
273
+ const visitedNodes = new Set(); // Stores "type:id" keys
274
+
275
+ // Seed with matched entities from query text
276
+ for (const entId of matchedEntityIds) {
277
+ const key = `entity:${entId}`;
278
+ if (!visitedNodes.has(key)) {
279
+ visitedNodes.add(key);
280
+ hopQueue.push({ id: entId, type: 'entity', depth: 0 });
281
+ }
282
+ }
283
+
284
+ // Seed with search hit memories
285
+ for (const hit of searchHits) {
286
+ const key = `memory:${hit.id}`;
287
+ if (!visitedNodes.has(key)) {
288
+ visitedNodes.add(key);
289
+ hopQueue.push({ id: hit.id, type: 'memory', depth: 0 });
290
+ }
291
+ }
292
+
293
+ // BFS to traverse memories and entities uniformly up to depth 6
294
+ while (hopQueue.length > 0) {
295
+ const { id, type, depth } = hopQueue.shift();
296
+ if (depth >= 6) continue;
257
297
 
258
- // 2. Perform Graph Hop (multi-hop traversal)
259
- // Find all entities directly connected to this search hit memory
260
- const hitEdges = db.prepare(`
298
+ // --- 2a. Explicit Graph Edges (from edges table) ---
299
+ const connectedEdges = db.prepare(`
261
300
  SELECT * FROM edges
262
- WHERE (source_id = ? AND source_type = 'memory' AND target_type = 'entity')
263
- OR (target_id = ? AND target_type = 'memory' AND source_type = 'entity')
264
- `).all(hit.id, hit.id);
265
-
266
- const startEntityIds = new Set();
267
- for (const edge of hitEdges) {
268
- if (edge.source_type === 'entity') startEntityIds.add(edge.source_id);
269
- if (edge.target_type === 'entity') startEntityIds.add(edge.target_id);
301
+ WHERE (source_id = ? AND source_type = ?)
302
+ OR (target_id = ? AND target_type = ?)
303
+ `).all(id, type, id, type);
304
+
305
+ for (const edge of connectedEdges) {
306
+ let nextId, nextType;
307
+ if (edge.source_id === id && edge.source_type === type) {
308
+ nextId = edge.target_id;
309
+ nextType = edge.target_type;
310
+ } else {
311
+ nextId = edge.source_id;
312
+ nextType = edge.source_type;
313
+ }
314
+
315
+ const key = `${nextType}:${nextId}`;
316
+ if (!visitedNodes.has(key)) {
317
+ visitedNodes.add(key);
318
+ hopQueue.push({ id: nextId, type: nextType, depth: depth + 1 });
319
+ }
270
320
  }
271
321
 
272
- // BFS to find connected entities up to depth 2 (entity -> entity -> entity)
273
- const visitedEntities = new Set(startEntityIds);
274
- const queue = Array.from(startEntityIds).map(id => ({ id, depth: 0 }));
275
-
276
- while (queue.length > 0) {
277
- const { id, depth } = queue.shift();
278
- if (depth >= 2) continue;
279
-
280
- const connectedEdges = db.prepare(`
281
- SELECT * FROM edges
282
- WHERE (source_id = ? AND source_type = 'entity' AND target_type = 'entity')
283
- OR (target_id = ? AND target_type = 'entity' AND source_type = 'entity')
284
- `).all(id, id);
285
-
286
- for (const edge of connectedEdges) {
287
- const nextId = edge.source_id === id ? edge.target_id : edge.source_id;
288
- if (!visitedEntities.has(nextId)) {
289
- visitedEntities.add(nextId);
290
- queue.push({ id: nextId, depth: depth + 1 });
322
+ // --- 2b. Implicit Name-Based Edges (for robustness when explicit edges are missing) ---
323
+ if (type === 'memory') {
324
+ const memoryRow = db.prepare('SELECT content FROM memories WHERE id = ?').get(id);
325
+ if (memoryRow && memoryRow.content) {
326
+ const contentLower = memoryRow.content.toLowerCase();
327
+ for (const ent of entities) {
328
+ if (contentLower.includes(ent.name.toLowerCase())) {
329
+ const nextKey = `entity:${ent.id}`;
330
+ if (!visitedNodes.has(nextKey)) {
331
+ visitedNodes.add(nextKey);
332
+ hopQueue.push({ id: ent.id, type: 'entity', depth: depth + 1 });
333
+ }
334
+ }
335
+ }
336
+ }
337
+ } else if (type === 'entity') {
338
+ const ent = entities.find(e => e.id === id);
339
+ if (ent && ent.name) {
340
+ const matchingMemories = db.prepare('SELECT id FROM memories WHERE content LIKE ? AND valid_until IS NULL').all(`%${ent.name}%`);
341
+ for (const row of matchingMemories) {
342
+ const nextKey = `memory:${row.id}`;
343
+ if (!visitedNodes.has(nextKey)) {
344
+ visitedNodes.add(nextKey);
345
+ hopQueue.push({ id: row.id, type: 'memory', depth: depth + 1 });
346
+ }
291
347
  }
292
348
  }
293
349
  }
350
+ }
294
351
 
295
- // Now collect all memories connected to any of the traversed entities
296
- for (const entId of visitedEntities) {
297
- const otherMemories = getMemoriesByEntity(entId);
298
- for (const other of otherMemories) {
299
- if (other.id === hit.id) continue;
300
- if (candidates.has(other.id)) continue;
301
-
302
- const otherProv = getProvenance(other.id);
303
- candidates.set(other.id, {
304
- id: other.id,
305
- content: other.content,
306
- importance_score: other.importance_score,
307
- created_at: other.created_at,
308
- last_accessed: other.last_accessed,
309
- score: parseFloat(hit.hybrid_score) * 0.5, // 50% graph-hop penalty
310
- provenance: otherProv,
311
- source: 'hop'
312
- });
352
+ // Now collect all hopped memories from the visited nodes
353
+ for (const key of visitedNodes) {
354
+ const [type, idStr] = key.split(':');
355
+ if (type === 'memory') {
356
+ const memId = Number(idStr);
357
+ if (candidates.has(memId)) continue; // Keep search hit info
358
+
359
+ // Check namespace filter if present
360
+ const other = getMemoryById(memId, namespace);
361
+ if (!other) continue;
362
+
363
+ let baseScore = 0.4;
364
+ if (searchHits.length > 0) {
365
+ const maxSearchScore = Math.max(...searchHits.map(h => parseFloat(h.hybrid_score)));
366
+ baseScore = maxSearchScore * 0.5;
313
367
  }
368
+
369
+ const otherProv = getProvenance(memId);
370
+ candidates.set(memId, {
371
+ id: other.id,
372
+ content: other.content,
373
+ importance_score: other.importance_score,
374
+ created_at: other.created_at,
375
+ last_accessed: other.last_accessed,
376
+ score: baseScore,
377
+ provenance: otherProv,
378
+ source: 'hop'
379
+ });
314
380
  }
315
381
  }
316
382
 
@@ -408,7 +474,7 @@ function checkRelationship(a, b) {
408
474
  }
409
475
 
410
476
  // Contradiction: similar topic, differing key terms
411
- if (jaccard > 0.15 && jaccard < 0.5) {
477
+ if (jaccard > 0.15 && jaccard < 0.65) {
412
478
  return { type: 'contradiction' };
413
479
  }
414
480
 
@@ -453,14 +519,14 @@ export async function consolidateMemories(namespace = null) {
453
519
  SELECT rowid AS id, distance
454
520
  FROM memories_vec
455
521
  WHERE embedding MATCH ?
456
- AND k = 10
522
+ AND k = 30
457
523
  `).all(embedding.embedding);
458
524
 
459
525
  const group = [];
460
526
  for (const hit of hits) {
461
527
  if (visited.has(Number(hit.id))) continue;
462
528
  const sim = Math.max(0, 1 - (hit.distance * hit.distance) / 2);
463
- if (sim > 0.85) {
529
+ if (sim > 0.80) {
464
530
  const other = db.prepare('SELECT * FROM memories WHERE id = ? AND valid_until IS NULL').get(Number(hit.id));
465
531
  if (other) {
466
532
  group.push(other);
@@ -481,7 +547,7 @@ export async function consolidateMemories(namespace = null) {
481
547
  };
482
548
 
483
549
  const groupWithTrust = group.map(m => ({ ...m, trust: getTrust(m) }));
484
- groupWithTrust.sort((a, b) => b.trust - a.trust || b.importance_score - a.importance_score || b.id - a.id);
550
+ groupWithTrust.sort((a, b) => b.trust - a.trust || b.importance_score - a.importance_score || a.id - b.id);
485
551
 
486
552
  // Resolve the group sequentially
487
553
  let canonical = groupWithTrust[0];
package/src/server.js CHANGED
@@ -1,30 +1,30 @@
1
1
  /**
2
- * server.js — MCP Server Setup
2
+ * server.js — MCP Server & Local HTTP Gateway Setup
3
3
  *
4
- * Creates the MCP server, registers all tools, and connects
5
- * via stdio transport (the standard MCP communication method).
6
- * Sets up hourly temporal decay and daily consolidation background tasks.
4
+ * Creates the MCP server, registers all tools, and connects via stdio.
5
+ * Also spins up a local HTTP/JSON Gateway on port 4321 to support low-latency
6
+ * prompt hooks and local agent swarms without subprocess overhead.
7
7
  *
8
- * IMPORTANT: Never write to stdout — it's reserved for MCP protocol.
9
8
  * All logging goes to stderr via console.error().
10
9
  */
11
10
 
11
+ import http from 'http';
12
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
- import { registerTools, cleanupWatchers } from './tools.js';
14
+ import { registerTools, cleanupWatchers, addMemoryInternal, executeToolInternal } from './tools.js';
15
15
  import { applyTemporalDecay, closeDatabase } from './database.js';
16
- import { consolidateMemories } from './search.js';
16
+ import { consolidateMemories, searchHybrid, getOptimizedContext } from './search.js';
17
17
  import { startWatcher, stopWatcher } from './watcher.js';
18
+ import { verifyChainIntegrity } from './attestation.js';
18
19
 
19
20
  /**
20
- * Start the Persyst MCP server.
21
- * This is called from index.js (the entry point).
21
+ * Start the Persyst MCP server & HTTP Gateway.
22
22
  */
23
23
  export async function startServer() {
24
24
  // --- Create MCP server ---
25
25
  const server = new McpServer({
26
26
  name: 'persyst',
27
- version: '2.1.3'
27
+ version: '2.2.1'
28
28
  });
29
29
 
30
30
  // --- Register all tools ---
@@ -34,12 +34,114 @@ export async function startServer() {
34
34
  // --- Start background log watcher daemon ---
35
35
  startWatcher();
36
36
 
37
+ // --- Start local HTTP Gateway (port 4321) ---
38
+ const httpPort = 4321;
39
+ const httpServer = http.createServer((req, res) => {
40
+ // CORS headers for local swarms and browser testing
41
+ res.setHeader('Access-Control-Allow-Origin', '*');
42
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
43
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
44
+
45
+ if (req.method === 'OPTIONS') {
46
+ res.writeHead(204);
47
+ res.end();
48
+ return;
49
+ }
50
+
51
+ if (req.method !== 'POST') {
52
+ res.writeHead(405, { 'Content-Type': 'application/json' });
53
+ res.end(JSON.stringify({ error: 'Method Not Allowed. Use POST.' }));
54
+ return;
55
+ }
56
+
57
+ let body = '';
58
+ req.on('data', chunk => { body += chunk; });
59
+ req.on('end', async () => {
60
+ try {
61
+ const payload = JSON.parse(body || '{}');
62
+
63
+ if (req.url === '/search') {
64
+ const { query, limit = 5, agent_id, session_id } = payload;
65
+ if (!query) {
66
+ res.writeHead(400, { 'Content-Type': 'application/json' });
67
+ res.end(JSON.stringify({ error: 'Missing required field: query' }));
68
+ return;
69
+ }
70
+ const results = await searchHybrid(query, limit, agent_id, session_id, agent_id || null);
71
+ res.writeHead(200, { 'Content-Type': 'application/json' });
72
+ res.end(JSON.stringify({ success: true, results }));
73
+ return;
74
+ }
75
+
76
+ if (req.url === '/add') {
77
+ const { content, importance = 1.0, agent_id, session_id, shared = true } = payload;
78
+ if (!content) {
79
+ res.writeHead(400, { 'Content-Type': 'application/json' });
80
+ res.end(JSON.stringify({ error: 'Missing required field: content' }));
81
+ return;
82
+ }
83
+ const result = await addMemoryInternal({ content, importance, agent_id, session_id, shared });
84
+ if (result.error) {
85
+ res.writeHead(400, { 'Content-Type': 'application/json' });
86
+ } else {
87
+ res.writeHead(200, { 'Content-Type': 'application/json' });
88
+ }
89
+ res.end(JSON.stringify(result));
90
+ return;
91
+ }
92
+
93
+ if (req.url === '/context') {
94
+ const { query, max_tokens = 2000, agent_id, session_id } = payload;
95
+ if (!query) {
96
+ res.writeHead(400, { 'Content-Type': 'application/json' });
97
+ res.end(JSON.stringify({ error: 'Missing required field: query' }));
98
+ return;
99
+ }
100
+ const context = await getOptimizedContext(query, max_tokens, agent_id, session_id);
101
+ res.writeHead(200, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify(context));
103
+ return;
104
+ }
105
+
106
+ if (req.url === '/tool') {
107
+ const { name, arguments: args } = payload;
108
+ if (!name) {
109
+ res.writeHead(400, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({ error: 'Missing required field: name' }));
111
+ return;
112
+ }
113
+ const result = await executeToolInternal(name, args || {});
114
+ res.writeHead(200, { 'Content-Type': 'application/json' });
115
+ res.end(JSON.stringify(result));
116
+ return;
117
+ }
118
+
119
+ if (req.url === '/verify') {
120
+ const result = await verifyChainIntegrity();
121
+ res.writeHead(200, { 'Content-Type': 'application/json' });
122
+ res.end(JSON.stringify(result));
123
+ return;
124
+ }
125
+
126
+ res.writeHead(404, { 'Content-Type': 'application/json' });
127
+ res.end(JSON.stringify({ error: 'Endpoint Not Found' }));
128
+ } catch (err) {
129
+ res.writeHead(500, { 'Content-Type': 'application/json' });
130
+ res.end(JSON.stringify({ error: err.message }));
131
+ }
132
+ });
133
+ });
134
+
135
+ httpServer.listen(httpPort, '127.0.0.1', () => {
136
+ console.error(`[persyst] HTTP Gateway listening on http://127.0.0.1:${httpPort} ✓`);
137
+ });
138
+
37
139
  // --- Start temporal decay timer ---
38
140
  // Runs every hour: reduces importance of memories not accessed in 7+ days
39
141
  const decayTimer = setInterval(applyTemporalDecay, 3600000);
40
142
 
41
143
  // --- Start daily consolidation sweep ---
42
- // Runs every 24 hours: merges similar memories (similarity > 0.85)
144
+ // Runs every 24 hours: merges similar memories
43
145
  const consolidationTimer = setInterval(async () => {
44
146
  console.error('[persyst] Running scheduled daily memory consolidation sweep...');
45
147
  try {
@@ -50,13 +152,14 @@ export async function startServer() {
50
152
  }
51
153
  }, 86400000);
52
154
 
53
- // --- Graceful shutdown (Bug 3 fix: also cleans up git watchers) ---
155
+ // --- Graceful shutdown ---
54
156
  const shutdown = () => {
55
157
  console.error('[persyst] Shutting down...');
56
158
  clearInterval(decayTimer);
57
159
  clearInterval(consolidationTimer);
58
160
  stopWatcher(); // Stop background log watcher
59
- cleanupWatchers(); // Bug 3 fix: stop all git repo watchers
161
+ cleanupWatchers(); // Stop all git repo watchers
162
+ httpServer.close(); // Close HTTP gateway
60
163
  closeDatabase();
61
164
  process.exit(0);
62
165
  };