server-memory-enhanced 3.0.0 → 3.2.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, ReadGraphInputSchema, SearchNodesInputSchema, OpenNodesInputSchema, QueryNodesInputSchema, GetMemoryStatsInputSchema, GetRecentChangesInputSchema, FindRelationPathInputSchema, DetectConflictsInputSchema, GetFlaggedEntitiesInputSchema, GetContextInputSchema } 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, CreateEntitiesInputSchema, CreateRelationsInputSchema, AddObservationsInputSchema, DeleteEntitiesInputSchema, DeleteObservationsInputSchema, DeleteRelationsInputSchema, PruneMemoryInputSchema, BulkUpdateInputSchema, FlagForReviewInputSchema } 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';
@@ -115,7 +115,7 @@ server.registerTool("save_memory", {
115
115
  inputSchema: SaveMemoryInputSchema,
116
116
  outputSchema: SaveMemoryOutputSchema
117
117
  }, async (input) => {
118
- const result = await handleSaveMemory(input, (entities) => knowledgeGraphManager.createEntities(entities), (relations) => knowledgeGraphManager.createRelations(relations), (threadId) => knowledgeGraphManager.getEntityNamesInThread(threadId));
118
+ const result = await handleSaveMemory(input, (threadId, entities) => knowledgeGraphManager.createEntities(threadId, entities), (threadId, relations) => knowledgeGraphManager.createRelations(threadId, relations), (threadId) => knowledgeGraphManager.getEntityNamesInThread(threadId));
119
119
  if (result.success) {
120
120
  // Build success message with entity names
121
121
  let successText = `✓ Successfully saved ${result.created.entities} entities and ${result.created.relations} relations.\n` +
@@ -171,14 +171,13 @@ server.registerTool("save_memory", {
171
171
  server.registerTool("create_entities", {
172
172
  title: "Create Entities",
173
173
  description: "Create multiple new entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance)",
174
- inputSchema: {
175
- entities: z.array(EntitySchemaCompat)
176
- },
174
+ inputSchema: CreateEntitiesInputSchema,
177
175
  outputSchema: {
178
176
  entities: z.array(EntitySchemaCompat)
179
177
  }
180
- }, async ({ entities }) => {
181
- const result = await knowledgeGraphManager.createEntities(entities);
178
+ }, async (input) => {
179
+ const { threadId, entities } = input;
180
+ const result = await knowledgeGraphManager.createEntities(threadId, entities);
182
181
  return {
183
182
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
184
183
  structuredContent: { entities: result }
@@ -188,14 +187,13 @@ server.registerTool("create_entities", {
188
187
  server.registerTool("create_relations", {
189
188
  title: "Create Relations",
190
189
  description: "Create multiple new relations between entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance). Relations should be in active voice",
191
- inputSchema: {
192
- relations: z.array(RelationSchemaCompat)
193
- },
190
+ inputSchema: CreateRelationsInputSchema,
194
191
  outputSchema: {
195
192
  relations: z.array(RelationSchemaCompat)
196
193
  }
197
- }, async ({ relations }) => {
198
- const result = await knowledgeGraphManager.createRelations(relations);
194
+ }, async (input) => {
195
+ const { threadId, relations } = input;
196
+ const result = await knowledgeGraphManager.createRelations(threadId, relations);
199
197
  return {
200
198
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
201
199
  structuredContent: { relations: result }
@@ -205,24 +203,16 @@ server.registerTool("create_relations", {
205
203
  server.registerTool("add_observations", {
206
204
  title: "Add Observations",
207
205
  description: "Add new observations to existing entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance)",
208
- inputSchema: {
209
- observations: z.array(z.object({
210
- entityName: z.string().describe("The name of the entity to add the observations to"),
211
- contents: z.array(z.string()).describe("An array of observation contents to add"),
212
- agentThreadId: z.string().describe("The agent thread ID adding these observations"),
213
- timestamp: z.string().describe("ISO 8601 timestamp of when the observations are added"),
214
- confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
215
- importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
216
- }))
217
- },
206
+ inputSchema: AddObservationsInputSchema,
218
207
  outputSchema: {
219
208
  results: z.array(z.object({
220
209
  entityName: z.string(),
221
210
  addedObservations: z.array(z.string())
222
211
  }))
223
212
  }
224
- }, async ({ observations }) => {
225
- const result = await knowledgeGraphManager.addObservations(observations);
213
+ }, async (input) => {
214
+ const { threadId, observations } = input;
215
+ const result = await knowledgeGraphManager.addObservations(threadId, observations);
226
216
  return {
227
217
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
228
218
  structuredContent: { results: result }
@@ -232,15 +222,14 @@ server.registerTool("add_observations", {
232
222
  server.registerTool("delete_entities", {
233
223
  title: "Delete Entities",
234
224
  description: "Delete multiple entities and their associated relations from the knowledge graph",
235
- inputSchema: {
236
- entityNames: z.array(z.string()).describe("An array of entity names to delete")
237
- },
225
+ inputSchema: DeleteEntitiesInputSchema,
238
226
  outputSchema: {
239
227
  success: z.boolean(),
240
228
  message: z.string()
241
229
  }
242
- }, async ({ entityNames }) => {
243
- await knowledgeGraphManager.deleteEntities(entityNames);
230
+ }, async (input) => {
231
+ const { threadId, entityNames } = input;
232
+ await knowledgeGraphManager.deleteEntities(threadId, entityNames);
244
233
  return {
245
234
  content: [{ type: "text", text: "Entities deleted successfully" }],
246
235
  structuredContent: { success: true, message: "Entities deleted successfully" }
@@ -250,18 +239,14 @@ server.registerTool("delete_entities", {
250
239
  server.registerTool("delete_observations", {
251
240
  title: "Delete Observations",
252
241
  description: "Delete specific observations from entities in the knowledge graph",
253
- inputSchema: {
254
- deletions: z.array(z.object({
255
- entityName: z.string().describe("The name of the entity containing the observations"),
256
- observations: z.array(z.string()).describe("An array of observations to delete")
257
- }))
258
- },
242
+ inputSchema: DeleteObservationsInputSchema,
259
243
  outputSchema: {
260
244
  success: z.boolean(),
261
245
  message: z.string()
262
246
  }
263
- }, async ({ deletions }) => {
264
- await knowledgeGraphManager.deleteObservations(deletions);
247
+ }, async (input) => {
248
+ const { threadId, deletions } = input;
249
+ await knowledgeGraphManager.deleteObservations(threadId, deletions);
265
250
  return {
266
251
  content: [{ type: "text", text: "Observations deleted successfully" }],
267
252
  structuredContent: { success: true, message: "Observations deleted successfully" }
@@ -273,7 +258,8 @@ server.registerTool("update_observation", {
273
258
  description: "Update an existing observation by creating a new version with updated content. This maintains version history through the supersedes/superseded_by chain.",
274
259
  inputSchema: UpdateObservationInputSchema.shape,
275
260
  outputSchema: UpdateObservationOutputSchema.shape
276
- }, async ({ entityName, observationId, newContent, agentThreadId, timestamp, confidence, importance }) => {
261
+ }, async (input) => {
262
+ const { entityName, observationId, newContent, agentThreadId, timestamp, confidence, importance } = input;
277
263
  const updatedObservation = await knowledgeGraphManager.updateObservation({
278
264
  entityName,
279
265
  observationId,
@@ -297,15 +283,14 @@ server.registerTool("update_observation", {
297
283
  server.registerTool("delete_relations", {
298
284
  title: "Delete Relations",
299
285
  description: "Delete multiple relations from the knowledge graph",
300
- inputSchema: {
301
- relations: z.array(RelationSchemaCompat).describe("An array of relations to delete")
302
- },
286
+ inputSchema: DeleteRelationsInputSchema,
303
287
  outputSchema: {
304
288
  success: z.boolean(),
305
289
  message: z.string()
306
290
  }
307
- }, async ({ relations }) => {
308
- await knowledgeGraphManager.deleteRelations(relations);
291
+ }, async (input) => {
292
+ const { threadId, relations } = input;
293
+ await knowledgeGraphManager.deleteRelations(threadId, relations);
309
294
  return {
310
295
  content: [{ type: "text", text: "Relations deleted successfully" }],
311
296
  structuredContent: { success: true, message: "Relations deleted successfully" }
@@ -314,14 +299,15 @@ server.registerTool("delete_relations", {
314
299
  // Register read_graph tool
315
300
  server.registerTool("read_graph", {
316
301
  title: "Read Graph",
317
- description: "Read the knowledge graph for a specific thread (thread isolation enforced)",
302
+ description: "Read the knowledge graph for a specific thread (thread isolation enforced). Supports filtering by minimum importance threshold.",
318
303
  inputSchema: ReadGraphInputSchema,
319
304
  outputSchema: {
320
305
  entities: z.array(EntitySchemaCompat),
321
306
  relations: z.array(RelationSchemaCompat)
322
307
  }
323
308
  }, async (input) => {
324
- const graph = await knowledgeGraphManager.readGraph(input.threadId);
309
+ const { threadId, minImportance } = input;
310
+ const graph = await knowledgeGraphManager.readGraph(threadId, minImportance);
325
311
  return {
326
312
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
327
313
  structuredContent: { ...graph }
@@ -592,17 +578,14 @@ server.registerTool("detect_conflicts", {
592
578
  server.registerTool("prune_memory", {
593
579
  title: "Prune Memory",
594
580
  description: "Remove old or low-importance entities to manage memory size, with option to keep minimum number of entities",
595
- inputSchema: {
596
- olderThan: z.string().optional().describe("ISO 8601 timestamp - remove entities older than this"),
597
- importanceLessThan: z.number().min(0).max(1).optional().describe("Remove entities with importance less than this value"),
598
- keepMinEntities: z.number().optional().describe("Minimum number of entities to keep regardless of filters")
599
- },
581
+ inputSchema: PruneMemoryInputSchema,
600
582
  outputSchema: {
601
583
  removedEntities: z.number(),
602
584
  removedRelations: z.number()
603
585
  }
604
- }, async (options) => {
605
- const result = await knowledgeGraphManager.pruneMemory(options);
586
+ }, async (input) => {
587
+ const { threadId, ...options } = input;
588
+ const result = await knowledgeGraphManager.pruneMemory(threadId, options);
606
589
  return {
607
590
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
608
591
  structuredContent: result
@@ -612,20 +595,14 @@ server.registerTool("prune_memory", {
612
595
  server.registerTool("bulk_update", {
613
596
  title: "Bulk Update",
614
597
  description: "Efficiently update multiple entities at once with new confidence, importance, or observations",
615
- inputSchema: {
616
- updates: z.array(z.object({
617
- entityName: z.string(),
618
- confidence: z.number().min(0).max(1).optional(),
619
- importance: z.number().min(0).max(1).optional(),
620
- addObservations: z.array(z.string()).optional()
621
- }))
622
- },
598
+ inputSchema: BulkUpdateInputSchema,
623
599
  outputSchema: {
624
600
  updated: z.number(),
625
601
  notFound: z.array(z.string())
626
602
  }
627
- }, async ({ updates }) => {
628
- const result = await knowledgeGraphManager.bulkUpdate(updates);
603
+ }, async (input) => {
604
+ const { threadId, updates } = input;
605
+ const result = await knowledgeGraphManager.bulkUpdate(threadId, updates);
629
606
  return {
630
607
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
631
608
  structuredContent: result
@@ -635,17 +612,14 @@ server.registerTool("bulk_update", {
635
612
  server.registerTool("flag_for_review", {
636
613
  title: "Flag Entity for Review",
637
614
  description: "Mark an entity for human review with a specific reason (Human-in-the-Loop)",
638
- inputSchema: {
639
- entityName: z.string().describe("Name of entity to flag"),
640
- reason: z.string().describe("Reason for flagging"),
641
- reviewer: z.string().optional().describe("Optional reviewer name")
642
- },
615
+ inputSchema: FlagForReviewInputSchema,
643
616
  outputSchema: {
644
617
  success: z.boolean(),
645
618
  message: z.string()
646
619
  }
647
- }, async ({ entityName, reason, reviewer }) => {
648
- await knowledgeGraphManager.flagForReview(entityName, reason, reviewer);
620
+ }, async (input) => {
621
+ const { threadId, entityName, reason, reviewer } = input;
622
+ await knowledgeGraphManager.flagForReview(threadId, entityName, reason, reviewer);
649
623
  return {
650
624
  content: [{ type: "text", text: `Entity "${entityName}" flagged for review` }],
651
625
  structuredContent: { success: true, message: `Entity "${entityName}" flagged for review` }
@@ -4,12 +4,13 @@
4
4
  import { randomUUID } from 'crypto';
5
5
  /**
6
6
  * Flag an entity for review
7
+ * Thread isolation: Only flags entities in the specified thread
7
8
  */
8
- export async function flagForReview(storage, entityName, reason, reviewer) {
9
+ export async function flagForReview(storage, threadId, entityName, reason, reviewer) {
9
10
  const graph = await storage.loadGraph();
10
- const entity = graph.entities.find(e => e.name === entityName);
11
+ const entity = graph.entities.find(e => e.name === entityName && e.agentThreadId === threadId);
11
12
  if (!entity) {
12
- throw new Error(`Entity with name ${entityName} not found`);
13
+ throw new Error(`Entity with name ${entityName} not found in thread ${threadId}`);
13
14
  }
14
15
  // Add a special observation to mark for review
15
16
  const flagContent = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
@@ -43,39 +43,43 @@ export class KnowledgeGraphManager {
43
43
  await this.initializePromise;
44
44
  }
45
45
  // Entity Operations
46
- async createEntities(entities) {
46
+ async createEntities(threadId, entities) {
47
47
  await this.ensureInitialized();
48
- return EntityOps.createEntities(this.storage, entities);
48
+ return EntityOps.createEntities(this.storage, threadId, entities);
49
49
  }
50
- async deleteEntities(entityNames) {
50
+ async deleteEntities(threadId, entityNames) {
51
51
  await this.ensureInitialized();
52
- return EntityOps.deleteEntities(this.storage, entityNames);
52
+ return EntityOps.deleteEntities(this.storage, threadId, entityNames);
53
53
  }
54
54
  // Relation Operations
55
- async createRelations(relations) {
55
+ async createRelations(threadId, relations) {
56
56
  await this.ensureInitialized();
57
- return RelationOps.createRelations(this.storage, relations);
57
+ return RelationOps.createRelations(this.storage, threadId, relations);
58
58
  }
59
- async deleteRelations(relations) {
59
+ async deleteRelations(threadId, relations) {
60
60
  await this.ensureInitialized();
61
- return RelationOps.deleteRelations(this.storage, relations);
61
+ return RelationOps.deleteRelations(this.storage, threadId, relations);
62
62
  }
63
63
  // Observation Operations
64
- async addObservations(observations) {
64
+ async addObservations(threadId, observations) {
65
65
  await this.ensureInitialized();
66
- return ObservationOps.addObservations(this.storage, observations);
66
+ return ObservationOps.addObservations(this.storage, threadId, observations);
67
67
  }
68
- async deleteObservations(deletions) {
68
+ async deleteObservations(threadId, deletions) {
69
69
  await this.ensureInitialized();
70
- return ObservationOps.deleteObservations(this.storage, deletions);
70
+ return ObservationOps.deleteObservations(this.storage, threadId, deletions);
71
71
  }
72
72
  async updateObservation(params) {
73
73
  await this.ensureInitialized();
74
74
  return ObservationOps.updateObservation(this.storage, params);
75
75
  }
76
76
  // Graph Reading Operations
77
- async readGraph(threadId) {
77
+ async readGraph(threadId, minImportance) {
78
78
  await this.ensureInitialized();
79
+ // Pass minImportance if provided, otherwise let readGraph use its default
80
+ if (minImportance !== undefined) {
81
+ return GraphReader.readGraph(this.storage, threadId, minImportance);
82
+ }
79
83
  return GraphReader.readGraph(this.storage, threadId);
80
84
  }
81
85
  // Search Operations
@@ -131,18 +135,18 @@ export class KnowledgeGraphManager {
131
135
  return AnalyticsService.getAnalytics(this.storage, threadId);
132
136
  }
133
137
  // Memory Maintenance
134
- async pruneMemory(options) {
138
+ async pruneMemory(threadId, options) {
135
139
  await this.ensureInitialized();
136
- return MemoryPruner.pruneMemory(this.storage, options);
140
+ return MemoryPruner.pruneMemory(this.storage, threadId, options);
137
141
  }
138
- async bulkUpdate(updates) {
142
+ async bulkUpdate(threadId, updates) {
139
143
  await this.ensureInitialized();
140
- return BulkUpdater.bulkUpdate(this.storage, updates);
144
+ return BulkUpdater.bulkUpdate(this.storage, threadId, updates);
141
145
  }
142
146
  // Collaboration Features
143
- async flagForReview(entityName, reason, reviewer) {
147
+ async flagForReview(threadId, entityName, reason, reviewer) {
144
148
  await this.ensureInitialized();
145
- return FlagManager.flagForReview(this.storage, entityName, reason, reviewer);
149
+ return FlagManager.flagForReview(this.storage, threadId, entityName, reason, reviewer);
146
150
  }
147
151
  async getFlaggedEntities(threadId) {
148
152
  await this.ensureInitialized();
@@ -4,13 +4,14 @@
4
4
  import { randomUUID } from 'crypto';
5
5
  /**
6
6
  * Perform bulk updates on multiple entities
7
+ * Thread isolation: Only updates entities in the specified thread
7
8
  */
8
- export async function bulkUpdate(storage, updates) {
9
+ export async function bulkUpdate(storage, threadId, updates) {
9
10
  const graph = await storage.loadGraph();
10
11
  let updated = 0;
11
12
  const notFound = [];
12
13
  for (const update of updates) {
13
- const entity = graph.entities.find(e => e.name === update.entityName);
14
+ const entity = graph.entities.find(e => e.name === update.entityName && e.agentThreadId === threadId);
14
15
  if (!entity) {
15
16
  notFound.push(update.entityName);
16
17
  continue;
@@ -3,13 +3,18 @@
3
3
  */
4
4
  /**
5
5
  * Prune memory based on age and importance criteria
6
+ * Thread isolation: Only prunes entities and relations in the specified thread
6
7
  */
7
- export async function pruneMemory(storage, options) {
8
+ export async function pruneMemory(storage, threadId, options) {
8
9
  const graph = await storage.loadGraph();
9
- const initialEntityCount = graph.entities.length;
10
- const initialRelationCount = graph.relations.length;
11
- // Filter entities to remove
12
- let entitiesToKeep = graph.entities;
10
+ // Filter to only entities in the specified thread
11
+ const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
12
+ const initialEntityCount = threadEntities.length;
13
+ // Count initial relations in the thread
14
+ const threadEntityNames = new Set(threadEntities.map(e => e.name));
15
+ const initialRelationCount = graph.relations.filter(r => r.agentThreadId === threadId && threadEntityNames.has(r.from) && threadEntityNames.has(r.to)).length;
16
+ // Filter entities to remove within the thread
17
+ let entitiesToKeep = threadEntities;
13
18
  if (options.olderThan) {
14
19
  const cutoffDate = new Date(options.olderThan);
15
20
  entitiesToKeep = entitiesToKeep.filter(e => new Date(e.timestamp) >= cutoffDate);
@@ -18,13 +23,13 @@ export async function pruneMemory(storage, options) {
18
23
  entitiesToKeep = entitiesToKeep.filter(e => e.importance >= options.importanceLessThan);
19
24
  }
20
25
  // Ensure we keep minimum entities
21
- // If keepMinEntities is set and we need more entities, backfill from the original graph
26
+ // If keepMinEntities is set and we need more entities, backfill from the original thread entities
22
27
  // sorted by importance and recency
23
28
  if (options.keepMinEntities && entitiesToKeep.length < options.keepMinEntities) {
24
29
  const minToKeep = options.keepMinEntities;
25
30
  const alreadyKeptNames = new Set(entitiesToKeep.map(e => e.name));
26
- // Candidates are entities from the original graph that are not already kept
27
- const candidates = graph.entities
31
+ // Candidates are entities from the thread that are not already kept
32
+ const candidates = threadEntities
28
33
  .filter(e => !alreadyKeptNames.has(e.name))
29
34
  .sort((a, b) => {
30
35
  if (a.importance !== b.importance)
@@ -36,13 +41,17 @@ export async function pruneMemory(storage, options) {
36
41
  entitiesToKeep = [...entitiesToKeep, ...backfill];
37
42
  }
38
43
  const keptEntityNames = new Set(entitiesToKeep.map(e => e.name));
39
- // Remove relations that reference removed entities
40
- const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
41
- graph.entities = entitiesToKeep;
42
- graph.relations = relationsToKeep;
44
+ const removedEntityNames = new Set(threadEntityNames);
45
+ keptEntityNames.forEach(name => removedEntityNames.delete(name));
46
+ // Update the main graph: remove entities from this thread that should be pruned
47
+ graph.entities = graph.entities.filter(e => e.agentThreadId !== threadId || keptEntityNames.has(e.name));
48
+ // Remove relations from this thread that reference removed entities
49
+ graph.relations = graph.relations.filter(r => !(r.agentThreadId === threadId && (removedEntityNames.has(r.from) || removedEntityNames.has(r.to))));
43
50
  await storage.saveGraph(graph);
51
+ // Count remaining relations in the thread after pruning
52
+ const finalRelationCount = graph.relations.filter(r => r.agentThreadId === threadId && keptEntityNames.has(r.from) && keptEntityNames.has(r.to)).length;
44
53
  return {
45
54
  removedEntities: initialEntityCount - entitiesToKeep.length,
46
- removedRelations: initialRelationCount - relationsToKeep.length
55
+ removedRelations: initialRelationCount - finalRelationCount
47
56
  };
48
57
  }
@@ -5,10 +5,12 @@
5
5
  * Create new entities in the knowledge graph
6
6
  * Entity names are globally unique across all threads in the collaborative knowledge graph
7
7
  * This prevents duplicate entities while allowing multiple threads to contribute to the same entity
8
+ * @param threadId - Thread ID passed for context (entities already have agentThreadId set)
8
9
  */
9
- export async function createEntities(storage, entities) {
10
+ export async function createEntities(storage, threadId, entities) {
10
11
  const graph = await storage.loadGraph();
11
12
  const existingNames = new Set(graph.entities.map(e => e.name));
13
+ // Filter out entities that already exist, entities are expected to have agentThreadId already set
12
14
  const newEntities = entities.filter(e => !existingNames.has(e.name));
13
15
  graph.entities.push(...newEntities);
14
16
  await storage.saveGraph(graph);
@@ -17,11 +19,18 @@ export async function createEntities(storage, entities) {
17
19
  /**
18
20
  * Delete entities from the knowledge graph
19
21
  * Also removes all relations referencing the deleted entities
22
+ * Thread isolation: Only deletes entities that belong to the specified thread
20
23
  */
21
- export async function deleteEntities(storage, entityNames) {
24
+ export async function deleteEntities(storage, threadId, entityNames) {
22
25
  const graph = await storage.loadGraph();
23
26
  const namesToDelete = new Set(entityNames);
24
- graph.entities = graph.entities.filter(e => !namesToDelete.has(e.name));
25
- graph.relations = graph.relations.filter(r => !namesToDelete.has(r.from) && !namesToDelete.has(r.to));
27
+ // Determine which entities will actually be deleted for this thread
28
+ const deletedEntityNames = new Set(graph.entities
29
+ .filter(e => namesToDelete.has(e.name) && e.agentThreadId === threadId)
30
+ .map(e => e.name));
31
+ // Delete entities that were identified for deletion
32
+ graph.entities = graph.entities.filter(e => !deletedEntityNames.has(e.name));
33
+ // Only delete relations that belong to the specified thread and reference actually deleted entities
34
+ graph.relations = graph.relations.filter(r => !(r.agentThreadId === threadId && (deletedEntityNames.has(r.from) || deletedEntityNames.has(r.to))));
26
35
  await storage.saveGraph(graph);
27
36
  }
@@ -7,13 +7,15 @@ import { validateObservationNotSuperseded, createObservationVersion } from '../u
7
7
  /**
8
8
  * Add observations to entities
9
9
  * Checks for duplicate content and creates version chains when content is updated
10
+ * Thread parameter is used for validation to ensure only entities in the thread are modified
10
11
  */
11
- export async function addObservations(storage, observations) {
12
+ export async function addObservations(storage, threadId, observations) {
12
13
  const graph = await storage.loadGraph();
13
14
  const results = observations.map(o => {
14
- const entity = graph.entities.find(e => e.name === o.entityName);
15
+ // Find entity - thread validation happens here to ensure we only modify entities from this thread
16
+ const entity = graph.entities.find(e => e.name === o.entityName && e.agentThreadId === threadId);
15
17
  if (!entity) {
16
- throw new Error(`Entity with name ${o.entityName} not found`);
18
+ throw new Error(`Entity with name ${o.entityName} not found in thread ${threadId}`);
17
19
  }
18
20
  // Check for existing observations with same content to create version chain
19
21
  const newObservations = [];
@@ -56,11 +58,13 @@ export async function addObservations(storage, observations) {
56
58
  /**
57
59
  * Delete observations from entities
58
60
  * Supports deletion by content (backward compatibility) or by ID
61
+ * Thread parameter is used for validation to ensure only entities in the thread are modified
59
62
  */
60
- export async function deleteObservations(storage, deletions) {
63
+ export async function deleteObservations(storage, threadId, deletions) {
61
64
  const graph = await storage.loadGraph();
62
65
  deletions.forEach(d => {
63
- const entity = graph.entities.find(e => e.name === d.entityName);
66
+ // Find entity - thread validation happens here to ensure we only modify entities from this thread
67
+ const entity = graph.entities.find(e => e.name === d.entityName && e.agentThreadId === threadId);
64
68
  if (entity) {
65
69
  // Delete observations by content (for backward compatibility) or by ID
66
70
  entity.observations = entity.observations.filter(o => !d.observations.includes(o.content) && !d.observations.includes(o.id));
@@ -6,8 +6,9 @@ import { createRelationKey } from '../utils/relation-key.js';
6
6
  * Create new relations in the knowledge graph
7
7
  * Relations are globally unique by (from, to, relationType) across all threads
8
8
  * This enables multiple threads to collaboratively build the knowledge graph
9
+ * @param threadId - Thread ID passed for context (relations already have agentThreadId set)
9
10
  */
10
- export async function createRelations(storage, relations) {
11
+ export async function createRelations(storage, threadId, relations) {
11
12
  const graph = await storage.loadGraph();
12
13
  // Validate that referenced entities exist
13
14
  const entityNames = new Set(graph.entities.map(e => e.name));
@@ -33,13 +34,14 @@ export async function createRelations(storage, relations) {
33
34
  }
34
35
  /**
35
36
  * Delete relations from the knowledge graph
36
- * Deletions affect all threads in the collaborative knowledge graph
37
+ * Thread isolation: Only deletes relations that belong to the specified thread
37
38
  */
38
- export async function deleteRelations(storage, relations) {
39
+ export async function deleteRelations(storage, threadId, relations) {
39
40
  const graph = await storage.loadGraph();
40
- // Delete relations globally across all threads by matching (from, to, relationType)
41
+ // Delete relations only from the specified thread by matching (from, to, relationType, threadId)
41
42
  graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
42
43
  r.to === delRelation.to &&
43
- r.relationType === delRelation.relationType));
44
+ r.relationType === delRelation.relationType &&
45
+ r.agentThreadId === threadId));
44
46
  await storage.saveGraph(graph);
45
47
  }