server-memory-enhanced 2.3.2 → 3.0.0

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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { promises as fs } from 'fs';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
9
- import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema } from './lib/schemas.js';
9
+ import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema, ReadGraphInputSchema, SearchNodesInputSchema, OpenNodesInputSchema, QueryNodesInputSchema, GetMemoryStatsInputSchema, GetRecentChangesInputSchema, FindRelationPathInputSchema, DetectConflictsInputSchema, GetFlaggedEntitiesInputSchema, GetContextInputSchema } from './lib/schemas.js';
10
10
  import { handleSaveMemory } from './lib/save-memory-handler.js';
11
11
  import { validateSaveMemoryRequest } from './lib/validation.js';
12
12
  import { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
@@ -314,14 +314,14 @@ server.registerTool("delete_relations", {
314
314
  // Register read_graph tool
315
315
  server.registerTool("read_graph", {
316
316
  title: "Read Graph",
317
- description: "Read the entire knowledge graph",
318
- inputSchema: {},
317
+ description: "Read the knowledge graph for a specific thread (thread isolation enforced)",
318
+ inputSchema: ReadGraphInputSchema,
319
319
  outputSchema: {
320
320
  entities: z.array(EntitySchemaCompat),
321
321
  relations: z.array(RelationSchemaCompat)
322
322
  }
323
- }, async () => {
324
- const graph = await knowledgeGraphManager.readGraph();
323
+ }, async (input) => {
324
+ const graph = await knowledgeGraphManager.readGraph(input.threadId);
325
325
  return {
326
326
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
327
327
  structuredContent: { ...graph }
@@ -330,16 +330,14 @@ server.registerTool("read_graph", {
330
330
  // Register search_nodes tool
331
331
  server.registerTool("search_nodes", {
332
332
  title: "Search Nodes",
333
- description: "Search for nodes in the knowledge graph based on a query",
334
- inputSchema: {
335
- query: z.string().describe("The search query to match against entity names, types, and observation content")
336
- },
333
+ description: "Search for nodes in the knowledge graph based on a query (thread isolation enforced)",
334
+ inputSchema: SearchNodesInputSchema,
337
335
  outputSchema: {
338
336
  entities: z.array(EntitySchemaCompat),
339
337
  relations: z.array(RelationSchemaCompat)
340
338
  }
341
- }, async ({ query }) => {
342
- const graph = await knowledgeGraphManager.searchNodes(query);
339
+ }, async (input) => {
340
+ const graph = await knowledgeGraphManager.searchNodes(input.threadId, input.query);
343
341
  return {
344
342
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
345
343
  structuredContent: { ...graph }
@@ -348,16 +346,14 @@ server.registerTool("search_nodes", {
348
346
  // Register open_nodes tool
349
347
  server.registerTool("open_nodes", {
350
348
  title: "Open Nodes",
351
- description: "Open specific nodes in the knowledge graph by their names",
352
- inputSchema: {
353
- names: z.array(z.string()).describe("An array of entity names to retrieve")
354
- },
349
+ description: "Open specific nodes in the knowledge graph by their names (thread isolation enforced)",
350
+ inputSchema: OpenNodesInputSchema,
355
351
  outputSchema: {
356
352
  entities: z.array(EntitySchemaCompat),
357
353
  relations: z.array(RelationSchemaCompat)
358
354
  }
359
- }, async ({ names }) => {
360
- const graph = await knowledgeGraphManager.openNodes(names);
355
+ }, async (input) => {
356
+ const graph = await knowledgeGraphManager.openNodes(input.threadId, input.names);
361
357
  return {
362
358
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
363
359
  structuredContent: { ...graph }
@@ -366,21 +362,15 @@ server.registerTool("open_nodes", {
366
362
  // Register query_nodes tool for advanced filtering
367
363
  server.registerTool("query_nodes", {
368
364
  title: "Query Nodes",
369
- description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges",
370
- inputSchema: {
371
- timestampStart: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or after this time"),
372
- timestampEnd: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or before this time"),
373
- confidenceMin: z.number().min(0).max(1).optional().describe("Minimum confidence value (0-1)"),
374
- confidenceMax: z.number().min(0).max(1).optional().describe("Maximum confidence value (0-1)"),
375
- importanceMin: z.number().min(0).max(1).optional().describe("Minimum importance value (0-1)"),
376
- importanceMax: z.number().min(0).max(1).optional().describe("Maximum importance value (0-1)")
377
- },
365
+ description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges (thread isolation enforced)",
366
+ inputSchema: QueryNodesInputSchema,
378
367
  outputSchema: {
379
368
  entities: z.array(EntitySchemaCompat),
380
369
  relations: z.array(RelationSchemaCompat)
381
370
  }
382
- }, async (filters) => {
383
- const graph = await knowledgeGraphManager.queryNodes(filters);
371
+ }, async (input) => {
372
+ const { threadId, ...filters } = input;
373
+ const graph = await knowledgeGraphManager.queryNodes(threadId, Object.keys(filters).length > 0 ? filters : undefined);
384
374
  return {
385
375
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
386
376
  structuredContent: { ...graph }
@@ -389,7 +379,7 @@ server.registerTool("query_nodes", {
389
379
  // Register list_entities tool for simple entity lookup
390
380
  server.registerTool("list_entities", {
391
381
  title: "List Entities",
392
- description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery.",
382
+ description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery. Thread isolation enforced - only shows entities from the specified thread.",
393
383
  inputSchema: ListEntitiesInputSchema,
394
384
  outputSchema: ListEntitiesOutputSchema
395
385
  }, async (input) => {
@@ -522,8 +512,8 @@ server.registerTool("validate_memory", {
522
512
  // Register get_memory_stats tool
523
513
  server.registerTool("get_memory_stats", {
524
514
  title: "Get Memory Statistics",
525
- description: "Get comprehensive statistics about the knowledge graph including entity counts, thread activity, and confidence/importance metrics",
526
- inputSchema: {},
515
+ description: "Get comprehensive statistics about the knowledge graph for a specific thread including entity counts, activity, and confidence/importance metrics (thread isolation enforced)",
516
+ inputSchema: GetMemoryStatsInputSchema,
527
517
  outputSchema: {
528
518
  entityCount: z.number(),
529
519
  relationCount: z.number(),
@@ -536,8 +526,8 @@ server.registerTool("get_memory_stats", {
536
526
  entityCount: z.number()
537
527
  }))
538
528
  }
539
- }, async () => {
540
- const stats = await knowledgeGraphManager.getMemoryStats();
529
+ }, async (input) => {
530
+ const stats = await knowledgeGraphManager.getMemoryStats(input.threadId);
541
531
  return {
542
532
  content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
543
533
  structuredContent: stats
@@ -546,16 +536,14 @@ server.registerTool("get_memory_stats", {
546
536
  // Register get_recent_changes tool
547
537
  server.registerTool("get_recent_changes", {
548
538
  title: "Get Recent Changes",
549
- description: "Retrieve entities and relations that were created or modified since a specific timestamp",
550
- inputSchema: {
551
- since: z.string().describe("ISO 8601 timestamp - return changes since this time")
552
- },
539
+ description: "Retrieve entities and relations that were created or modified since a specific timestamp (thread isolation enforced)",
540
+ inputSchema: GetRecentChangesInputSchema,
553
541
  outputSchema: {
554
542
  entities: z.array(EntitySchemaCompat),
555
543
  relations: z.array(RelationSchemaCompat)
556
544
  }
557
- }, async ({ since }) => {
558
- const changes = await knowledgeGraphManager.getRecentChanges(since);
545
+ }, async (input) => {
546
+ const changes = await knowledgeGraphManager.getRecentChanges(input.threadId, input.since);
559
547
  return {
560
548
  content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
561
549
  structuredContent: { ...changes }
@@ -564,19 +552,15 @@ server.registerTool("get_recent_changes", {
564
552
  // Register find_relation_path tool
565
553
  server.registerTool("find_relation_path", {
566
554
  title: "Find Relationship Path",
567
- description: "Find a path of relationships connecting two entities in the knowledge graph",
568
- inputSchema: {
569
- from: z.string().describe("Starting entity name"),
570
- to: z.string().describe("Target entity name"),
571
- maxDepth: z.number().optional().default(5).describe("Maximum path depth to search (default: 5)")
572
- },
555
+ description: "Find a path of relationships connecting two entities in the knowledge graph (thread isolation enforced)",
556
+ inputSchema: FindRelationPathInputSchema,
573
557
  outputSchema: {
574
558
  found: z.boolean(),
575
559
  path: z.array(z.string()),
576
560
  relations: z.array(RelationSchemaCompat)
577
561
  }
578
- }, async ({ from, to, maxDepth }) => {
579
- const result = await knowledgeGraphManager.findRelationPath(from, to, maxDepth || 5);
562
+ }, async (input) => {
563
+ const result = await knowledgeGraphManager.findRelationPath(input.threadId, input.from, input.to, input.maxDepth || 5);
580
564
  return {
581
565
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
582
566
  structuredContent: result
@@ -585,8 +569,8 @@ server.registerTool("find_relation_path", {
585
569
  // Register detect_conflicts tool
586
570
  server.registerTool("detect_conflicts", {
587
571
  title: "Detect Conflicts",
588
- description: "Detect potentially conflicting observations within entities using pattern matching and negation detection",
589
- inputSchema: {},
572
+ description: "Detect potentially conflicting observations within entities using pattern matching and negation detection (thread isolation enforced)",
573
+ inputSchema: DetectConflictsInputSchema,
590
574
  outputSchema: {
591
575
  conflicts: z.array(z.object({
592
576
  entityName: z.string(),
@@ -597,8 +581,8 @@ server.registerTool("detect_conflicts", {
597
581
  }))
598
582
  }))
599
583
  }
600
- }, async () => {
601
- const conflicts = await knowledgeGraphManager.detectConflicts();
584
+ }, async (input) => {
585
+ const conflicts = await knowledgeGraphManager.detectConflicts(input.threadId);
602
586
  return {
603
587
  content: [{ type: "text", text: JSON.stringify({ conflicts }, null, 2) }],
604
588
  structuredContent: { conflicts }
@@ -670,13 +654,13 @@ server.registerTool("flag_for_review", {
670
654
  // Register get_flagged_entities tool
671
655
  server.registerTool("get_flagged_entities", {
672
656
  title: "Get Flagged Entities",
673
- description: "Retrieve all entities that have been flagged for human review",
674
- inputSchema: {},
657
+ description: "Retrieve all entities that have been flagged for human review (thread isolation enforced)",
658
+ inputSchema: GetFlaggedEntitiesInputSchema,
675
659
  outputSchema: {
676
660
  entities: z.array(EntitySchemaCompat)
677
661
  }
678
- }, async () => {
679
- const entities = await knowledgeGraphManager.getFlaggedEntities();
662
+ }, async (input) => {
663
+ const entities = await knowledgeGraphManager.getFlaggedEntities(input.threadId);
680
664
  return {
681
665
  content: [{ type: "text", text: JSON.stringify({ entities }, null, 2) }],
682
666
  structuredContent: { entities }
@@ -685,17 +669,14 @@ server.registerTool("get_flagged_entities", {
685
669
  // Register get_context tool
686
670
  server.registerTool("get_context", {
687
671
  title: "Get Context",
688
- description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics",
689
- inputSchema: {
690
- entityNames: z.array(z.string()).describe("Names of entities to get context for"),
691
- depth: z.number().optional().default(1).describe("How many relationship hops to include (default: 1)")
692
- },
672
+ description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics (thread isolation enforced)",
673
+ inputSchema: GetContextInputSchema,
693
674
  outputSchema: {
694
675
  entities: z.array(EntitySchemaCompat),
695
676
  relations: z.array(RelationSchemaCompat)
696
677
  }
697
- }, async ({ entityNames, depth }) => {
698
- const context = await knowledgeGraphManager.getContext(entityNames, depth || 1);
678
+ }, async (input) => {
679
+ const context = await knowledgeGraphManager.getContext(input.threadId, input.entityNames, input.depth || 1);
699
680
  return {
700
681
  content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
701
682
  structuredContent: { ...context }
@@ -738,11 +719,11 @@ server.registerTool("get_analytics", {
738
719
  // Register get_observation_history tool
739
720
  server.registerTool("get_observation_history", {
740
721
  title: "Get Observation History",
741
- description: "Retrieve the full version chain for a specific observation, showing how it evolved over time",
722
+ description: "Retrieve the full version chain for a specific observation, showing how it evolved over time (thread isolation enforced)",
742
723
  inputSchema: GetObservationHistoryInputSchema,
743
724
  outputSchema: GetObservationHistoryOutputSchema
744
725
  }, async (input) => {
745
- const result = await knowledgeGraphManager.getObservationHistory(input.entityName, input.observationId);
726
+ const result = await knowledgeGraphManager.getObservationHistory(input.threadId, input.entityName, input.observationId);
746
727
  return {
747
728
  content: [{ type: "text", text: JSON.stringify({ history: result }, null, 2) }],
748
729
  structuredContent: { history: result }
@@ -5,11 +5,13 @@ import { hasNegation, NEGATION_WORDS } from '../utils/negation-detector.js';
5
5
  /**
6
6
  * Detect conflicting observations within entities
7
7
  * Identifies potential contradictions by checking for negation patterns
8
+ * Filtered by threadId for thread isolation
8
9
  */
9
- export async function detectConflicts(storage) {
10
+ export async function detectConflicts(storage, threadId) {
10
11
  const graph = await storage.loadGraph();
11
12
  const conflicts = [];
12
- for (const entity of graph.entities) {
13
+ // Only analyze entities in the specified thread
14
+ for (const entity of graph.entities.filter(e => e.agentThreadId === threadId)) {
13
15
  const entityConflicts = [];
14
16
  for (let i = 0; i < entity.observations.length; i++) {
15
17
  for (let j = i + 1; j < entity.observations.length; j++) {
@@ -4,16 +4,18 @@
4
4
  /**
5
5
  * Get context (entities related to specified entities up to a certain depth)
6
6
  * Expands to include related entities up to specified depth
7
+ * Filtered by threadId for thread isolation
7
8
  */
8
- export async function getContext(storage, entityNames, depth = 1) {
9
+ export async function getContext(storage, threadId, entityNames, depth = 1) {
9
10
  const graph = await storage.loadGraph();
10
11
  const contextEntityNames = new Set(entityNames);
11
- // Expand to include related entities up to specified depth
12
+ // Expand to include related entities up to specified depth - only within this thread
12
13
  for (let d = 0; d < depth; d++) {
13
14
  const currentEntities = Array.from(contextEntityNames);
14
15
  for (const entityName of currentEntities) {
15
- // Find all relations involving this entity
16
- const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
16
+ // Find all relations involving this entity - only from this thread
17
+ const relatedRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
18
+ (r.from === entityName || r.to === entityName));
17
19
  // Add related entities
18
20
  relatedRelations.forEach(r => {
19
21
  contextEntityNames.add(r.from);
@@ -21,9 +23,10 @@ export async function getContext(storage, entityNames, depth = 1) {
21
23
  });
22
24
  }
23
25
  }
24
- // Get all entities and relations in context
25
- const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
26
- const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
26
+ // Get all entities and relations in context - only from this thread
27
+ const contextEntities = graph.entities.filter(e => e.agentThreadId === threadId && contextEntityNames.has(e.name));
28
+ const contextRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
29
+ contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
27
30
  return {
28
31
  entities: contextEntities,
29
32
  relations: contextRelations
@@ -2,31 +2,34 @@
2
2
  * Memory statistics service
3
3
  */
4
4
  /**
5
- * Get comprehensive memory statistics
5
+ * Get comprehensive memory statistics for a specific thread
6
+ * Filtered by threadId for thread isolation
6
7
  */
7
- export async function getMemoryStats(storage) {
8
+ export async function getMemoryStats(storage, threadId) {
8
9
  const graph = await storage.loadGraph();
10
+ // Filter entities and relations by threadId
11
+ const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
12
+ const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
9
13
  // Count entity types
10
14
  const entityTypes = {};
11
- graph.entities.forEach(e => {
15
+ threadEntities.forEach(e => {
12
16
  entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
13
17
  });
14
18
  // Calculate averages
15
- const avgConfidence = graph.entities.length > 0
16
- ? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
19
+ const avgConfidence = threadEntities.length > 0
20
+ ? threadEntities.reduce((sum, e) => sum + e.confidence, 0) / threadEntities.length
17
21
  : 0;
18
- const avgImportance = graph.entities.length > 0
19
- ? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
22
+ const avgImportance = threadEntities.length > 0
23
+ ? threadEntities.reduce((sum, e) => sum + e.importance, 0) / threadEntities.length
20
24
  : 0;
21
- // Count unique threads
22
- const threads = new Set([
23
- ...graph.entities.map(e => e.agentThreadId),
24
- ...graph.relations.map(r => r.agentThreadId)
25
- ]);
26
- // Recent activity (last 7 days, grouped by day)
25
+ // Count unique threads in the system (across all entities in the graph)
26
+ const threads = new Set(graph.entities
27
+ .map(e => e.agentThreadId)
28
+ .filter((id) => !!id));
29
+ // Recent activity (last 7 days, grouped by day) - only for this thread
27
30
  const now = new Date();
28
31
  const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
29
- const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
32
+ const recentEntities = threadEntities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
30
33
  // Group by day
31
34
  const activityByDay = {};
32
35
  recentEntities.forEach(e => {
@@ -37,8 +40,8 @@ export async function getMemoryStats(storage) {
37
40
  .map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
38
41
  .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
39
42
  return {
40
- entityCount: graph.entities.length,
41
- relationCount: graph.relations.length,
43
+ entityCount: threadEntities.length,
44
+ relationCount: threadRelations.length,
42
45
  threadCount: threads.size,
43
46
  entityTypes,
44
47
  avgConfidence,
@@ -47,15 +50,16 @@ export async function getMemoryStats(storage) {
47
50
  };
48
51
  }
49
52
  /**
50
- * Get recent changes since a specific timestamp
53
+ * Get recent changes since a specific timestamp for a specific thread
54
+ * Filtered by threadId for thread isolation
51
55
  */
52
- export async function getRecentChanges(storage, since) {
56
+ export async function getRecentChanges(storage, threadId, since) {
53
57
  const graph = await storage.loadGraph();
54
58
  const sinceDate = new Date(since);
55
- // Only return entities and relations that were actually modified since the specified time
56
- const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
57
- // Only include relations that are recent themselves
58
- const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
59
+ // Only return entities and relations from this thread that were modified since the specified time
60
+ const recentEntities = graph.entities.filter(e => e.agentThreadId === threadId && new Date(e.timestamp) >= sinceDate);
61
+ // Only include relations from this thread that are recent themselves
62
+ const recentRelations = graph.relations.filter(r => r.agentThreadId === threadId && new Date(r.timestamp) >= sinceDate);
59
63
  return {
60
64
  entities: recentEntities,
61
65
  relations: recentRelations
@@ -4,16 +4,17 @@
4
4
  /**
5
5
  * Find the shortest path between two entities in the knowledge graph
6
6
  * Uses BFS algorithm with bidirectional search
7
+ * Filtered by threadId for thread isolation
7
8
  */
8
- export async function findRelationPath(storage, from, to, maxDepth = 5) {
9
+ export async function findRelationPath(storage, threadId, from, to, maxDepth = 5) {
9
10
  const graph = await storage.loadGraph();
10
11
  if (from === to) {
11
12
  return { found: true, path: [from], relations: [] };
12
13
  }
13
- // Build indexes for efficient relation lookup
14
+ // Build indexes for efficient relation lookup - only for relations in this thread
14
15
  const relationsFrom = new Map();
15
16
  const relationsTo = new Map();
16
- for (const rel of graph.relations) {
17
+ for (const rel of graph.relations.filter(r => r.agentThreadId === threadId)) {
17
18
  if (!relationsFrom.has(rel.from)) {
18
19
  relationsFrom.set(rel.from, []);
19
20
  }
@@ -30,9 +30,11 @@ export async function flagForReview(storage, entityName, reason, reviewer) {
30
30
  }
31
31
  }
32
32
  /**
33
- * Get all entities flagged for review
33
+ * Get all entities flagged for review in a specific thread
34
+ * Filtered by threadId for thread isolation
34
35
  */
35
- export async function getFlaggedEntities(storage) {
36
+ export async function getFlaggedEntities(storage, threadId) {
36
37
  const graph = await storage.loadGraph();
37
- return graph.entities.filter(e => e.observations.some(obs => obs.content.includes('[FLAGGED FOR REVIEW:')));
38
+ return graph.entities.filter(e => e.agentThreadId === threadId &&
39
+ e.observations.some(obs => obs.content.includes('[FLAGGED FOR REVIEW:')));
38
40
  }
@@ -74,22 +74,22 @@ export class KnowledgeGraphManager {
74
74
  return ObservationOps.updateObservation(this.storage, params);
75
75
  }
76
76
  // Graph Reading Operations
77
- async readGraph() {
77
+ async readGraph(threadId) {
78
78
  await this.ensureInitialized();
79
- return GraphReader.readGraph(this.storage);
79
+ return GraphReader.readGraph(this.storage, threadId);
80
80
  }
81
81
  // Search Operations
82
- async searchNodes(query) {
82
+ async searchNodes(threadId, query) {
83
83
  await this.ensureInitialized();
84
- return SearchService.searchNodes(this.storage, query);
84
+ return SearchService.searchNodes(this.storage, threadId, query);
85
85
  }
86
- async openNodes(names) {
86
+ async openNodes(threadId, names) {
87
87
  await this.ensureInitialized();
88
- return SearchService.openNodes(this.storage, names);
88
+ return SearchService.openNodes(this.storage, threadId, names);
89
89
  }
90
- async queryNodes(filters) {
90
+ async queryNodes(threadId, filters) {
91
91
  await this.ensureInitialized();
92
- return SearchService.queryNodes(this.storage, filters);
92
+ return SearchService.queryNodes(this.storage, threadId, filters);
93
93
  }
94
94
  // Entity Query Operations
95
95
  async getAllEntityNames() {
@@ -105,26 +105,26 @@ export class KnowledgeGraphManager {
105
105
  return EntityQueries.listEntities(this.storage, threadId, entityType, namePattern);
106
106
  }
107
107
  // Memory Statistics & Insights
108
- async getMemoryStats() {
108
+ async getMemoryStats(threadId) {
109
109
  await this.ensureInitialized();
110
- return MemoryStats.getMemoryStats(this.storage);
110
+ return MemoryStats.getMemoryStats(this.storage, threadId);
111
111
  }
112
- async getRecentChanges(since) {
112
+ async getRecentChanges(threadId, since) {
113
113
  await this.ensureInitialized();
114
- return MemoryStats.getRecentChanges(this.storage, since);
114
+ return MemoryStats.getRecentChanges(this.storage, threadId, since);
115
115
  }
116
116
  // Analysis Operations
117
- async findRelationPath(from, to, maxDepth = 5) {
117
+ async findRelationPath(threadId, from, to, maxDepth = 5) {
118
118
  await this.ensureInitialized();
119
- return PathFinder.findRelationPath(this.storage, from, to, maxDepth);
119
+ return PathFinder.findRelationPath(this.storage, threadId, from, to, maxDepth);
120
120
  }
121
- async detectConflicts() {
121
+ async detectConflicts(threadId) {
122
122
  await this.ensureInitialized();
123
- return ConflictDetector.detectConflicts(this.storage);
123
+ return ConflictDetector.detectConflicts(this.storage, threadId);
124
124
  }
125
- async getContext(entityNames, depth = 1) {
125
+ async getContext(threadId, entityNames, depth = 1) {
126
126
  await this.ensureInitialized();
127
- return ContextBuilder.getContext(this.storage, entityNames, depth);
127
+ return ContextBuilder.getContext(this.storage, threadId, entityNames, depth);
128
128
  }
129
129
  async getAnalytics(threadId) {
130
130
  await this.ensureInitialized();
@@ -144,17 +144,17 @@ export class KnowledgeGraphManager {
144
144
  await this.ensureInitialized();
145
145
  return FlagManager.flagForReview(this.storage, entityName, reason, reviewer);
146
146
  }
147
- async getFlaggedEntities() {
147
+ async getFlaggedEntities(threadId) {
148
148
  await this.ensureInitialized();
149
- return FlagManager.getFlaggedEntities(this.storage);
149
+ return FlagManager.getFlaggedEntities(this.storage, threadId);
150
150
  }
151
151
  async listConversations() {
152
152
  await this.ensureInitialized();
153
153
  return ConversationService.listConversations(this.storage);
154
154
  }
155
155
  // Observation Versioning
156
- async getObservationHistory(entityName, observationId) {
156
+ async getObservationHistory(threadId, entityName, observationId) {
157
157
  await this.ensureInitialized();
158
- return ObservationHistory.getObservationHistory(this.storage, entityName, observationId);
158
+ return ObservationHistory.getObservationHistory(this.storage, threadId, entityName, observationId);
159
159
  }
160
160
  }
@@ -2,55 +2,57 @@
2
2
  * Entity query operations
3
3
  */
4
4
  /**
5
- * Get names of all entities that can be referenced in relations.
6
- * @returns Set of entity names that exist in the graph.
5
+ * Get names of all entities across all threads.
7
6
  *
8
- * Note: Returns ALL entities globally because entity names are globally unique across
9
- * all threads in the collaborative knowledge graph (by design - see createEntities).
10
- * This enables any thread to reference any existing entity, supporting incremental
11
- * building and cross-thread collaboration. Thread-specific filtering is not needed
12
- * since entity names cannot conflict across threads.
7
+ * Note: This function is used for administrative purposes and returns entity names
8
+ * from all threads. Entity names are NOT globally unique - different threads can
9
+ * have entities with the same name. This function is primarily used by the manager's
10
+ * getAllEntityNames() method.
11
+ *
12
+ * For thread-isolated validation (e.g., in save_memory), use getEntityNamesInThread() instead.
13
13
  */
14
14
  export async function getAllEntityNames(storage) {
15
15
  const graph = await storage.loadGraph();
16
16
  const entityNames = new Set();
17
- // Return all entities in the graph that can be referenced
18
- // This allows incremental building: entities from previous save_memory calls
19
- // can be referenced in new calls, enabling cross-save entity relations
17
+ // Return all entity names from all threads
20
18
  for (const entity of graph.entities) {
21
19
  entityNames.add(entity.name);
22
20
  }
23
21
  return entityNames;
24
22
  }
25
23
  /**
26
- * @deprecated Use {@link getAllEntityNames} instead.
27
- *
28
- * This method is kept for backward compatibility. It accepts a threadId parameter
29
- * for API consistency but does not use it for filtering; it returns the same
30
- * global set of entity names as {@link getAllEntityNames}.
24
+ * Get names of entities in a specific thread for thread isolation.
25
+ * This ensures entities can only reference other entities in the same thread.
31
26
  *
32
27
  * @param storage Storage adapter
33
- * @param threadId The thread ID (accepted but not used)
34
- * @returns Set of entity names that exist in the graph
28
+ * @param threadId The thread ID to filter by
29
+ * @returns Set of entity names that exist in the thread
35
30
  */
36
31
  export async function getEntityNamesInThread(storage, threadId) {
37
- return getAllEntityNames(storage);
32
+ const graph = await storage.loadGraph();
33
+ const entityNames = new Set();
34
+ // Return only entities in the specified thread
35
+ for (const entity of graph.entities) {
36
+ if (entity.agentThreadId === threadId) {
37
+ entityNames.add(entity.name);
38
+ }
39
+ }
40
+ return entityNames;
38
41
  }
39
42
  /**
40
43
  * List entities with optional filtering by type and name pattern
44
+ * Thread isolation enforced - threadId is required.
45
+ *
41
46
  * @param storage Storage adapter
42
- * @param threadId Optional thread ID to filter by. If not provided, returns entities from all threads.
47
+ * @param threadId Thread ID to filter by (required for thread isolation)
43
48
  * @param entityType Optional entity type filter (exact match)
44
49
  * @param namePattern Optional name pattern filter (case-insensitive substring match)
45
50
  * @returns Array of entities with name and entityType
46
51
  */
47
52
  export async function listEntities(storage, threadId, entityType, namePattern) {
48
53
  const graph = await storage.loadGraph();
49
- let filteredEntities = graph.entities;
50
- // Filter by thread ID if specified (otherwise returns all threads)
51
- if (threadId) {
52
- filteredEntities = filteredEntities.filter(e => e.agentThreadId === threadId);
53
- }
54
+ // Filter by thread ID (required for thread isolation)
55
+ let filteredEntities = graph.entities.filter(e => e.agentThreadId === threadId);
54
56
  // Filter by entity type if specified
55
57
  if (entityType) {
56
58
  filteredEntities = filteredEntities.filter(e => e.entityType === entityType);
@@ -2,8 +2,19 @@
2
2
  * Graph reading operations
3
3
  */
4
4
  /**
5
- * Read the entire knowledge graph
5
+ * Read the knowledge graph filtered by threadId for thread isolation
6
6
  */
7
- export async function readGraph(storage) {
8
- return storage.loadGraph();
7
+ export async function readGraph(storage, threadId) {
8
+ const graph = await storage.loadGraph();
9
+ // Filter entities by threadId
10
+ const filteredEntities = graph.entities.filter(e => e.agentThreadId === threadId);
11
+ // Create a Set of filtered entity names for quick lookup
12
+ const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
13
+ // Filter relations to only include those between filtered entities and from the same thread
14
+ const filteredRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
15
+ filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
16
+ return {
17
+ entities: filteredEntities,
18
+ relations: filteredRelations
19
+ };
9
20
  }
@@ -4,17 +4,20 @@
4
4
  /**
5
5
  * Search for nodes in the knowledge graph by query string
6
6
  * Searches entity names, types, and observation content
7
+ * Filtered by threadId for thread isolation
7
8
  */
8
- export async function searchNodes(storage, query) {
9
+ export async function searchNodes(storage, threadId, query) {
9
10
  const graph = await storage.loadGraph();
10
- // Filter entities
11
- const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
12
- e.entityType.toLowerCase().includes(query.toLowerCase()) ||
13
- e.observations.some(o => o.content?.toLowerCase().includes(query.toLowerCase())));
11
+ // Filter entities by threadId first, then by search query
12
+ const filteredEntities = graph.entities.filter(e => e.agentThreadId === threadId &&
13
+ (e.name.toLowerCase().includes(query.toLowerCase()) ||
14
+ e.entityType.toLowerCase().includes(query.toLowerCase()) ||
15
+ e.observations.some(o => o.content?.toLowerCase().includes(query.toLowerCase()))));
14
16
  // Create a Set of filtered entity names for quick lookup
15
17
  const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
16
- // Filter relations to only include those between filtered entities
17
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
18
+ // Filter relations to only include those between filtered entities and from the same thread
19
+ const filteredRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
20
+ filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
18
21
  const filteredGraph = {
19
22
  entities: filteredEntities,
20
23
  relations: filteredRelations,
@@ -24,15 +27,17 @@ export async function searchNodes(storage, query) {
24
27
  /**
25
28
  * Open specific nodes by name
26
29
  * Returns a subgraph containing only the specified entities and relations between them
30
+ * Filtered by threadId for thread isolation
27
31
  */
28
- export async function openNodes(storage, names) {
32
+ export async function openNodes(storage, threadId, names) {
29
33
  const graph = await storage.loadGraph();
30
- // Filter entities
31
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
34
+ // Filter entities by threadId first, then by name
35
+ const filteredEntities = graph.entities.filter(e => e.agentThreadId === threadId && names.includes(e.name));
32
36
  // Create a Set of filtered entity names for quick lookup
33
37
  const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
34
- // Filter relations to only include those between filtered entities
35
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
38
+ // Filter relations to only include those between filtered entities and from the same thread
39
+ const filteredRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
40
+ filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
36
41
  const filteredGraph = {
37
42
  entities: filteredEntities,
38
43
  relations: filteredRelations,
@@ -42,15 +47,18 @@ export async function openNodes(storage, names) {
42
47
  /**
43
48
  * Query nodes with advanced filters
44
49
  * Supports filtering by timestamp range, confidence range, and importance range
50
+ * Filtered by threadId for thread isolation
45
51
  */
46
- export async function queryNodes(storage, filters) {
52
+ export async function queryNodes(storage, threadId, filters) {
47
53
  const graph = await storage.loadGraph();
48
- // If no filters provided, return entire graph
49
- if (!filters) {
50
- return graph;
51
- }
52
- // Apply filters to entities
54
+ // Apply filters to entities, starting with threadId filter
53
55
  const filteredEntities = graph.entities.filter(e => {
56
+ // Thread isolation filter - must match
57
+ if (e.agentThreadId !== threadId)
58
+ return false;
59
+ // Optional filters below
60
+ if (!filters)
61
+ return true;
54
62
  // Timestamp range filter
55
63
  if (filters.timestampStart && e.timestamp < filters.timestampStart)
56
64
  return false;
@@ -72,9 +80,15 @@ export async function queryNodes(storage, filters) {
72
80
  const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
73
81
  // Apply filters to relations (and ensure they connect filtered entities)
74
82
  const filteredRelations = graph.relations.filter(r => {
83
+ // Thread isolation filter - must match
84
+ if (r.agentThreadId !== threadId)
85
+ return false;
75
86
  // Must connect filtered entities
76
87
  if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to))
77
88
  return false;
89
+ // Optional filters below
90
+ if (!filters)
91
+ return true;
78
92
  // Timestamp range filter
79
93
  if (filters.timestampStart && r.timestamp < filters.timestampStart)
80
94
  return false;
@@ -50,8 +50,8 @@ export const SaveMemoryEntitySchema = z.object({
50
50
  importance: z.number().min(0).max(1).optional().default(0.5).describe("Importance for memory integrity (0-1)")
51
51
  });
52
52
  export const SaveMemoryInputSchema = z.object({
53
- entities: z.array(SaveMemoryEntitySchema).min(1).describe("Array of entities to save"),
54
- threadId: z.string().min(1).describe("Thread ID for this conversation/project")
53
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
54
+ entities: z.array(SaveMemoryEntitySchema).min(1).describe("Array of entities to save")
55
55
  });
56
56
  export const SaveMemoryOutputSchema = z.object({
57
57
  success: z.boolean(),
@@ -95,6 +95,7 @@ export const GetAnalyticsOutputSchema = z.object({
95
95
  });
96
96
  // Schema for get_observation_history tool (Observation Versioning section of spec)
97
97
  export const GetObservationHistoryInputSchema = z.object({
98
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
98
99
  entityName: z.string().min(1).describe("Name of the entity"),
99
100
  observationId: z.string().min(1).describe("ID of the observation to retrieve history for")
100
101
  });
@@ -103,7 +104,7 @@ export const GetObservationHistoryOutputSchema = z.object({
103
104
  });
104
105
  // Schema for list_entities tool (Simple Entity Lookup)
105
106
  export const ListEntitiesInputSchema = z.object({
106
- threadId: z.string().optional().describe("Filter by thread ID (optional - returns entities from all threads if not specified)"),
107
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
107
108
  entityType: z.string().optional().describe("Filter by entity type (e.g., 'Person', 'Service', 'Document')"),
108
109
  namePattern: z.string().optional().describe("Filter by name pattern (case-insensitive substring match)")
109
110
  });
@@ -115,8 +116,8 @@ export const ListEntitiesOutputSchema = z.object({
115
116
  });
116
117
  // Schema for validate_memory tool (Pre-Validation)
117
118
  export const ValidateMemoryInputSchema = z.object({
118
- entities: z.array(SaveMemoryEntitySchema).min(1).describe("Array of entities to validate"),
119
- threadId: z.string().min(1).describe("Thread ID for this conversation/project")
119
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
120
+ entities: z.array(SaveMemoryEntitySchema).min(1).describe("Array of entities to validate")
120
121
  });
121
122
  export const ValidateMemoryOutputSchema = z.object({
122
123
  all_valid: z.boolean().describe("True if all entities pass validation"),
@@ -131,10 +132,10 @@ export const ValidateMemoryOutputSchema = z.object({
131
132
  });
132
133
  // Schema for update_observation tool
133
134
  export const UpdateObservationInputSchema = z.object({
135
+ agentThreadId: z.string().min(1).describe("Agent thread ID making this update"),
134
136
  entityName: z.string().min(1).describe("Name of the entity containing the observation"),
135
137
  observationId: z.string().min(1).describe("ID of the observation to update"),
136
138
  newContent: z.string().min(1).max(300).describe("New content for the observation (max 300 chars). Minimum 1 character to allow short but valid observations like abbreviations or single words."),
137
- agentThreadId: z.string().min(1).describe("Agent thread ID making this update"),
138
139
  timestamp: z.string().describe("ISO 8601 timestamp of the update"),
139
140
  confidence: z.number().min(0).max(1).optional().describe("Optional confidence score (0-1), inherits from old observation if not provided"),
140
141
  importance: z.number().min(0).max(1).optional().describe("Optional importance score (0-1), inherits from old observation if not provided")
@@ -144,3 +145,57 @@ export const UpdateObservationOutputSchema = z.object({
144
145
  updatedObservation: ObservationSchema.describe("The new version of the observation"),
145
146
  message: z.string()
146
147
  });
148
+ // Schema for read_graph tool
149
+ export const ReadGraphInputSchema = z.object({
150
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project")
151
+ });
152
+ // Schema for search_nodes tool
153
+ export const SearchNodesInputSchema = z.object({
154
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
155
+ query: z.string().min(1).describe("Search query string")
156
+ });
157
+ // Schema for open_nodes tool
158
+ export const OpenNodesInputSchema = z.object({
159
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
160
+ names: z.array(z.string()).min(1).describe("Array of entity names to open")
161
+ });
162
+ // Schema for query_nodes tool
163
+ export const QueryNodesInputSchema = z.object({
164
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
165
+ timestampStart: z.string().optional().describe("Filter by start timestamp (ISO 8601)"),
166
+ timestampEnd: z.string().optional().describe("Filter by end timestamp (ISO 8601)"),
167
+ confidenceMin: z.number().min(0).max(1).optional().describe("Filter by minimum confidence (0-1)"),
168
+ confidenceMax: z.number().min(0).max(1).optional().describe("Filter by maximum confidence (0-1)"),
169
+ importanceMin: z.number().min(0).max(1).optional().describe("Filter by minimum importance (0-1)"),
170
+ importanceMax: z.number().min(0).max(1).optional().describe("Filter by maximum importance (0-1)")
171
+ });
172
+ // Schema for get_memory_stats tool
173
+ export const GetMemoryStatsInputSchema = z.object({
174
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project")
175
+ });
176
+ // Schema for get_recent_changes tool
177
+ export const GetRecentChangesInputSchema = z.object({
178
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
179
+ since: z.string().describe("ISO 8601 timestamp to get changes since")
180
+ });
181
+ // Schema for find_relation_path tool
182
+ export const FindRelationPathInputSchema = z.object({
183
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
184
+ from: z.string().min(1).describe("Source entity name"),
185
+ to: z.string().min(1).describe("Target entity name"),
186
+ maxDepth: z.number().int().min(1).optional().default(5).describe("Maximum path depth (default: 5)")
187
+ });
188
+ // Schema for detect_conflicts tool
189
+ export const DetectConflictsInputSchema = z.object({
190
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project")
191
+ });
192
+ // Schema for get_flagged_entities tool
193
+ export const GetFlaggedEntitiesInputSchema = z.object({
194
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project")
195
+ });
196
+ // Schema for get_context tool
197
+ export const GetContextInputSchema = z.object({
198
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
199
+ entityNames: z.array(z.string()).min(1).describe("Array of entity names to get context for"),
200
+ depth: z.number().int().min(1).optional().default(1).describe("Context depth (default: 1)")
201
+ });
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Observation history and versioning service
3
3
  */
4
- import { findEntity, findObservation } from '../utils/entity-finder.js';
5
4
  /**
6
5
  * Get full history chain for an observation
7
6
  * Traces backwards and forwards through the version chain
7
+ * Filtered by threadId for thread isolation
8
8
  */
9
- export async function getObservationHistory(storage, entityName, observationId) {
9
+ export async function getObservationHistory(storage, threadId, entityName, observationId) {
10
10
  const graph = await storage.loadGraph();
11
- // Find the entity
12
- const entity = findEntity(graph, entityName);
11
+ // Find the entity - only in the specified thread
12
+ const entity = graph.entities.find(e => e.name === entityName && e.agentThreadId === threadId);
13
+ if (!entity) {
14
+ throw new Error(`Entity '${entityName}' not found in thread '${threadId}'`);
15
+ }
13
16
  // Find the starting observation
14
- const startObs = findObservation(entity, observationId);
17
+ const startObs = entity.observations.find(o => o.id === observationId);
18
+ if (!startObs) {
19
+ throw new Error(`Observation '${observationId}' not found in entity '${entityName}'`);
20
+ }
15
21
  // Build the version chain
16
22
  const history = [];
17
23
  // Trace backwards to find all predecessors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-memory-enhanced",
3
- "version": "2.3.2",
3
+ "version": "3.0.0",
4
4
  "description": "Enhanced MCP server for memory with agent threading, timestamps, and confidence scoring",
5
5
  "license": "MIT",
6
6
  "mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",