server-memory-enhanced 2.3.1 → 2.3.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.
@@ -1,48 +1,37 @@
1
1
  /**
2
2
  * KnowledgeGraphManager - Main class for managing the knowledge graph
3
+ * Acts as a facade to coordinate operations across different services
3
4
  */
4
- import { randomUUID } from 'crypto';
5
5
  import { JsonlStorageAdapter } from './jsonl-storage-adapter.js';
6
+ // Import CRUD operations
7
+ import * as EntityOps from './operations/entity-operations.js';
8
+ import * as RelationOps from './operations/relation-operations.js';
9
+ import * as ObservationOps from './operations/observation-operations.js';
10
+ // Import query operations
11
+ import * as GraphReader from './queries/graph-reader.js';
12
+ import * as SearchService from './queries/search-service.js';
13
+ import * as EntityQueries from './queries/entity-queries.js';
14
+ // Import analysis services
15
+ import * as MemoryStats from './analysis/memory-stats.js';
16
+ import * as PathFinder from './analysis/path-finder.js';
17
+ import * as ConflictDetector from './analysis/conflict-detector.js';
18
+ import * as ContextBuilder from './analysis/context-builder.js';
19
+ import * as AnalyticsService from './analysis/analytics-service.js';
20
+ // Import maintenance services
21
+ import * as MemoryPruner from './maintenance/memory-pruner.js';
22
+ import * as BulkUpdater from './maintenance/bulk-updater.js';
23
+ // Import versioning services
24
+ import * as ObservationHistory from './versioning/observation-history.js';
25
+ // Import collaboration services
26
+ import * as FlagManager from './collaboration/flag-manager.js';
27
+ import * as ConversationService from './collaboration/conversation-service.js';
6
28
  export class KnowledgeGraphManager {
7
- static NEGATION_WORDS = new Set(['not', 'no', 'never', 'neither', 'none', 'doesn\'t', 'don\'t', 'isn\'t', 'aren\'t']);
8
29
  storage;
9
30
  initializePromise = null;
10
31
  constructor(memoryDirPath, storageAdapter) {
11
32
  this.storage = storageAdapter || new JsonlStorageAdapter(memoryDirPath);
12
33
  // Lazy initialization - will be called on first operation
13
34
  }
14
- /**
15
- * Check if content contains any negation words (using word boundary matching)
16
- * Handles punctuation and contractions, avoids creating intermediate Set for performance
17
- */
18
- hasNegation(content) {
19
- // Extract words using word boundary regex, preserving contractions (include apostrophes)
20
- const lowerContent = content.toLowerCase();
21
- const wordMatches = lowerContent.match(/\b[\w']+\b/g);
22
- if (!wordMatches) {
23
- return false;
24
- }
25
- // Check each word against negation words without creating intermediate Set
26
- for (const word of wordMatches) {
27
- if (KnowledgeGraphManager.NEGATION_WORDS.has(word)) {
28
- return true;
29
- }
30
- }
31
- return false;
32
- }
33
- /**
34
- * Create a composite key for relation deduplication.
35
- *
36
- * We explicitly normalize the components to primitive strings to ensure
37
- * stable serialization and to document the assumption that `from`, `to`,
38
- * and `relationType` are simple string identifiers.
39
- */
40
- createRelationKey(relation) {
41
- const from = String(relation.from);
42
- const to = String(relation.to);
43
- const relationType = String(relation.relationType);
44
- return JSON.stringify([from, to, relationType]);
45
- }
46
35
  /**
47
36
  * Ensure storage is initialized before any operation
48
37
  * This is called automatically by all public methods
@@ -53,838 +42,119 @@ export class KnowledgeGraphManager {
53
42
  }
54
43
  await this.initializePromise;
55
44
  }
56
- /**
57
- * Find an entity by name in the knowledge graph.
58
- * @param graph - The knowledge graph to search
59
- * @param entityName - Name of the entity to find
60
- * @returns The found entity
61
- * @throws Error if entity not found
62
- */
63
- findEntity(graph, entityName) {
64
- const entity = graph.entities.find(e => e.name === entityName);
65
- if (!entity) {
66
- throw new Error(`Entity '${entityName}' not found`);
67
- }
68
- return entity;
69
- }
70
- /**
71
- * Find an observation by ID within an entity.
72
- * @param entity - The entity containing the observation
73
- * @param observationId - ID of the observation to find
74
- * @returns The found observation
75
- * @throws Error if observation not found
76
- */
77
- findObservation(entity, observationId) {
78
- const observation = entity.observations.find(o => o.id === observationId);
79
- if (!observation) {
80
- throw new Error(`Observation '${observationId}' not found in entity '${entity.name}'`);
81
- }
82
- return observation;
83
- }
84
- /**
85
- * Validate that an observation can be updated (not already superseded).
86
- * @param observation - The observation to validate
87
- * @throws Error if observation has already been superseded
88
- */
89
- validateObservationNotSuperseded(observation) {
90
- if (observation.superseded_by) {
91
- throw new Error(`Observation '${observation.id}' has already been superseded by '${observation.superseded_by}'. Update the latest version instead.`);
92
- }
93
- }
94
- /**
95
- * Resolve confidence value using inheritance chain: params > observation > entity.
96
- * @param providedValue - Value provided in parameters (optional)
97
- * @param observationValue - Value from observation (optional)
98
- * @param entityValue - Value from entity (fallback)
99
- * @returns Resolved confidence value
100
- */
101
- resolveInheritedValue(providedValue, observationValue, entityValue) {
102
- return providedValue ?? observationValue ?? entityValue;
103
- }
104
- /**
105
- * Create a new observation version from an existing observation.
106
- * @param oldObs - The observation being updated
107
- * @param entity - The entity containing the observation
108
- * @param params - Update parameters
109
- * @returns New observation with incremented version
110
- */
111
- createObservationVersion(oldObs, entity, params) {
112
- return {
113
- id: `obs_${randomUUID()}`,
114
- content: params.newContent,
115
- timestamp: params.timestamp,
116
- version: oldObs.version + 1,
117
- supersedes: oldObs.id,
118
- agentThreadId: params.agentThreadId,
119
- confidence: this.resolveInheritedValue(params.confidence, oldObs.confidence, entity.confidence),
120
- importance: this.resolveInheritedValue(params.importance, oldObs.importance, entity.importance)
121
- };
122
- }
45
+ // Entity Operations
123
46
  async createEntities(entities) {
124
47
  await this.ensureInitialized();
125
- const graph = await this.storage.loadGraph();
126
- // Entity names are globally unique across all threads in the collaborative knowledge graph
127
- // This prevents duplicate entities while allowing multiple threads to contribute to the same entity
128
- const existingNames = new Set(graph.entities.map(e => e.name));
129
- const newEntities = entities.filter(e => !existingNames.has(e.name));
130
- graph.entities.push(...newEntities);
131
- await this.storage.saveGraph(graph);
132
- return newEntities;
48
+ return EntityOps.createEntities(this.storage, entities);
49
+ }
50
+ async deleteEntities(entityNames) {
51
+ await this.ensureInitialized();
52
+ return EntityOps.deleteEntities(this.storage, entityNames);
133
53
  }
54
+ // Relation Operations
134
55
  async createRelations(relations) {
135
- const graph = await this.storage.loadGraph();
136
- // Validate that referenced entities exist
137
- const entityNames = new Set(graph.entities.map(e => e.name));
138
- const validRelations = relations.filter(r => {
139
- if (!entityNames.has(r.from) || !entityNames.has(r.to)) {
140
- console.warn(`Skipping relation ${r.from} -> ${r.to}: one or both entities do not exist`);
141
- return false;
142
- }
143
- return true;
144
- });
145
- // Relations are globally unique by (from, to, relationType) across all threads
146
- // This enables multiple threads to collaboratively build the knowledge graph
147
- const existingRelationKeys = new Set(graph.relations.map(r => this.createRelationKey(r)));
148
- // Create composite keys once per valid relation to avoid duplicate serialization
149
- const validRelationsWithKeys = validRelations.map(r => ({
150
- relation: r,
151
- key: this.createRelationKey(r)
152
- }));
153
- const newRelations = validRelationsWithKeys
154
- .filter(item => !existingRelationKeys.has(item.key))
155
- .map(item => item.relation);
156
- graph.relations.push(...newRelations);
157
- await this.storage.saveGraph(graph);
158
- return newRelations;
56
+ await this.ensureInitialized();
57
+ return RelationOps.createRelations(this.storage, relations);
159
58
  }
160
- async addObservations(observations) {
161
- const graph = await this.storage.loadGraph();
162
- const results = observations.map(o => {
163
- const entity = graph.entities.find(e => e.name === o.entityName);
164
- if (!entity) {
165
- throw new Error(`Entity with name ${o.entityName} not found`);
166
- }
167
- // Check for existing observations with same content to create version chain
168
- const newObservations = [];
169
- // Build a Set of existing observation contents for efficient lookup (single-pass)
170
- const existingContents = entity.observations.reduce((set, obs) => {
171
- if (!obs.superseded_by) {
172
- set.add(obs.content);
173
- }
174
- return set;
175
- }, new Set());
176
- for (const content of o.contents) {
177
- // Check if observation with this content already exists (latest version)
178
- if (existingContents.has(content)) {
179
- // Don't add duplicate - observation with this content already exists
180
- // Versioning is for UPDATES to content, not for re-asserting the same content
181
- continue;
182
- }
183
- // Create brand new observation
184
- const newObs = {
185
- id: `obs_${randomUUID()}`,
186
- content: content,
187
- timestamp: o.timestamp,
188
- version: 1,
189
- agentThreadId: o.agentThreadId,
190
- confidence: o.confidence,
191
- importance: o.importance
192
- };
193
- entity.observations.push(newObs);
194
- newObservations.push(newObs);
195
- }
196
- // Update entity metadata
197
- entity.timestamp = o.timestamp;
198
- entity.confidence = Math.max(entity.confidence, o.confidence);
199
- entity.importance = Math.max(entity.importance, o.importance);
200
- return { entityName: o.entityName, addedObservations: newObservations };
201
- });
202
- await this.storage.saveGraph(graph);
203
- return results;
59
+ async deleteRelations(relations) {
60
+ await this.ensureInitialized();
61
+ return RelationOps.deleteRelations(this.storage, relations);
204
62
  }
205
- async deleteEntities(entityNames) {
206
- const graph = await this.storage.loadGraph();
207
- const namesToDelete = new Set(entityNames);
208
- graph.entities = graph.entities.filter(e => !namesToDelete.has(e.name));
209
- graph.relations = graph.relations.filter(r => !namesToDelete.has(r.from) && !namesToDelete.has(r.to));
210
- await this.storage.saveGraph(graph);
63
+ // Observation Operations
64
+ async addObservations(observations) {
65
+ await this.ensureInitialized();
66
+ return ObservationOps.addObservations(this.storage, observations);
211
67
  }
212
68
  async deleteObservations(deletions) {
213
- const graph = await this.storage.loadGraph();
214
- deletions.forEach(d => {
215
- const entity = graph.entities.find(e => e.name === d.entityName);
216
- if (entity) {
217
- // Delete observations by content (for backward compatibility) or by ID
218
- entity.observations = entity.observations.filter(o => !d.observations.includes(o.content) && !d.observations.includes(o.id));
219
- }
220
- });
221
- await this.storage.saveGraph(graph);
69
+ await this.ensureInitialized();
70
+ return ObservationOps.deleteObservations(this.storage, deletions);
222
71
  }
223
- /**
224
- * Update an existing observation by creating a new version with updated content.
225
- * This maintains the version history through the supersedes/superseded_by chain.
226
- *
227
- * @param params - Update parameters
228
- * @param params.entityName - Name of the entity containing the observation
229
- * @param params.observationId - ID of the observation to update
230
- * @param params.newContent - New content for the observation
231
- * @param params.agentThreadId - Agent thread ID making this update
232
- * @param params.timestamp - ISO 8601 timestamp of the update
233
- * @param params.confidence - Optional confidence score (0-1), inherits from old observation if not provided
234
- * @param params.importance - Optional importance score (0-1), inherits from old observation if not provided
235
- * @returns The newly created observation with incremented version number
236
- * @throws Error if entity not found
237
- * @throws Error if observation not found
238
- * @throws Error if observation has already been superseded (must update latest version)
239
- */
240
72
  async updateObservation(params) {
241
73
  await this.ensureInitialized();
242
- const graph = await this.storage.loadGraph();
243
- // Find and validate the entity and observation
244
- const entity = this.findEntity(graph, params.entityName);
245
- const oldObs = this.findObservation(entity, params.observationId);
246
- this.validateObservationNotSuperseded(oldObs);
247
- // Create new version with inheritance chain
248
- const newObs = this.createObservationVersion(oldObs, entity, params);
249
- // Link old observation to new one
250
- oldObs.superseded_by = newObs.id;
251
- // Add new observation to entity
252
- entity.observations.push(newObs);
253
- // Update entity timestamp
254
- entity.timestamp = params.timestamp;
255
- await this.storage.saveGraph(graph);
256
- return newObs;
257
- }
258
- async deleteRelations(relations) {
259
- const graph = await this.storage.loadGraph();
260
- // Delete relations globally across all threads by matching (from, to, relationType)
261
- // In a collaborative knowledge graph, deletions affect all threads
262
- graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
263
- r.to === delRelation.to &&
264
- r.relationType === delRelation.relationType));
265
- await this.storage.saveGraph(graph);
74
+ return ObservationOps.updateObservation(this.storage, params);
266
75
  }
76
+ // Graph Reading Operations
267
77
  async readGraph() {
268
- return this.storage.loadGraph();
78
+ await this.ensureInitialized();
79
+ return GraphReader.readGraph(this.storage);
269
80
  }
81
+ // Search Operations
270
82
  async searchNodes(query) {
271
- const graph = await this.storage.loadGraph();
272
- // Filter entities
273
- const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
274
- e.entityType.toLowerCase().includes(query.toLowerCase()) ||
275
- e.observations.some(o => o.content?.toLowerCase().includes(query.toLowerCase())));
276
- // Create a Set of filtered entity names for quick lookup
277
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
278
- // Filter relations to only include those between filtered entities
279
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
280
- const filteredGraph = {
281
- entities: filteredEntities,
282
- relations: filteredRelations,
283
- };
284
- return filteredGraph;
83
+ await this.ensureInitialized();
84
+ return SearchService.searchNodes(this.storage, query);
285
85
  }
286
86
  async openNodes(names) {
287
- const graph = await this.storage.loadGraph();
288
- // Filter entities
289
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
290
- // Create a Set of filtered entity names for quick lookup
291
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
292
- // Filter relations to only include those between filtered entities
293
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
294
- const filteredGraph = {
295
- entities: filteredEntities,
296
- relations: filteredRelations,
297
- };
298
- return filteredGraph;
87
+ await this.ensureInitialized();
88
+ return SearchService.openNodes(this.storage, names);
299
89
  }
300
90
  async queryNodes(filters) {
301
- const graph = await this.storage.loadGraph();
302
- // If no filters provided, return entire graph
303
- if (!filters) {
304
- return graph;
305
- }
306
- // Apply filters to entities
307
- const filteredEntities = graph.entities.filter(e => {
308
- // Timestamp range filter
309
- if (filters.timestampStart && e.timestamp < filters.timestampStart)
310
- return false;
311
- if (filters.timestampEnd && e.timestamp > filters.timestampEnd)
312
- return false;
313
- // Confidence range filter
314
- if (filters.confidenceMin !== undefined && e.confidence < filters.confidenceMin)
315
- return false;
316
- if (filters.confidenceMax !== undefined && e.confidence > filters.confidenceMax)
317
- return false;
318
- // Importance range filter
319
- if (filters.importanceMin !== undefined && e.importance < filters.importanceMin)
320
- return false;
321
- if (filters.importanceMax !== undefined && e.importance > filters.importanceMax)
322
- return false;
323
- return true;
324
- });
325
- // Create a Set of filtered entity names for quick lookup
326
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
327
- // Apply filters to relations (and ensure they connect filtered entities)
328
- const filteredRelations = graph.relations.filter(r => {
329
- // Must connect filtered entities
330
- if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to))
331
- return false;
332
- // Timestamp range filter
333
- if (filters.timestampStart && r.timestamp < filters.timestampStart)
334
- return false;
335
- if (filters.timestampEnd && r.timestamp > filters.timestampEnd)
336
- return false;
337
- // Confidence range filter
338
- if (filters.confidenceMin !== undefined && r.confidence < filters.confidenceMin)
339
- return false;
340
- if (filters.confidenceMax !== undefined && r.confidence > filters.confidenceMax)
341
- return false;
342
- // Importance range filter
343
- if (filters.importanceMin !== undefined && r.importance < filters.importanceMin)
344
- return false;
345
- if (filters.importanceMax !== undefined && r.importance > filters.importanceMax)
346
- return false;
347
- return true;
348
- });
349
- return {
350
- entities: filteredEntities,
351
- relations: filteredRelations,
352
- };
91
+ await this.ensureInitialized();
92
+ return SearchService.queryNodes(this.storage, filters);
353
93
  }
354
- /**
355
- * Get names of all entities that can be referenced in relations.
356
- * @returns Set of entity names that exist in the graph.
357
- *
358
- * Note: Returns ALL entities globally because entity names are globally unique across
359
- * all threads in the collaborative knowledge graph (by design - see createEntities).
360
- * This enables any thread to reference any existing entity, supporting incremental
361
- * building and cross-thread collaboration. Thread-specific filtering is not needed
362
- * since entity names cannot conflict across threads.
363
- */
94
+ // Entity Query Operations
364
95
  async getAllEntityNames() {
365
- const graph = await this.storage.loadGraph();
366
- const entityNames = new Set();
367
- // Return all entities in the graph that can be referenced
368
- // This allows incremental building: entities from previous save_memory calls
369
- // can be referenced in new calls, enabling cross-save entity relations
370
- for (const entity of graph.entities) {
371
- entityNames.add(entity.name);
372
- }
373
- return entityNames;
96
+ await this.ensureInitialized();
97
+ return EntityQueries.getAllEntityNames(this.storage);
374
98
  }
375
- /**
376
- * @deprecated Use {@link getAllEntityNames} instead.
377
- *
378
- * This method is kept for backward compatibility. It accepts a threadId parameter
379
- * for API consistency but does not use it for filtering; it returns the same
380
- * global set of entity names as {@link getAllEntityNames}.
381
- *
382
- * @param threadId The thread ID (accepted but not used)
383
- * @returns Set of entity names that exist in the graph
384
- */
385
99
  async getEntityNamesInThread(threadId) {
386
- return this.getAllEntityNames();
100
+ await this.ensureInitialized();
101
+ return EntityQueries.getEntityNamesInThread(this.storage, threadId);
387
102
  }
388
- /**
389
- * List entities with optional filtering by type and name pattern
390
- * @param threadId Optional thread ID to filter by. If not provided, returns entities from all threads.
391
- * @param entityType Optional entity type filter (exact match)
392
- * @param namePattern Optional name pattern filter (case-insensitive substring match)
393
- * @returns Array of entities with name and entityType
394
- */
395
103
  async listEntities(threadId, entityType, namePattern) {
396
- const graph = await this.storage.loadGraph();
397
- let filteredEntities = graph.entities;
398
- // Filter by thread ID if specified (otherwise returns all threads)
399
- if (threadId) {
400
- filteredEntities = filteredEntities.filter(e => e.agentThreadId === threadId);
401
- }
402
- // Filter by entity type if specified
403
- if (entityType) {
404
- filteredEntities = filteredEntities.filter(e => e.entityType === entityType);
405
- }
406
- // Filter by name pattern if specified (case-insensitive)
407
- if (namePattern) {
408
- const pattern = namePattern.toLowerCase();
409
- filteredEntities = filteredEntities.filter(e => e.name.toLowerCase().includes(pattern));
410
- }
411
- // Return simplified list with just name and entityType
412
- return filteredEntities.map(e => ({
413
- name: e.name,
414
- entityType: e.entityType
415
- }));
104
+ await this.ensureInitialized();
105
+ return EntityQueries.listEntities(this.storage, threadId, entityType, namePattern);
416
106
  }
417
- // Enhancement 1: Memory Statistics & Insights
107
+ // Memory Statistics & Insights
418
108
  async getMemoryStats() {
419
- const graph = await this.storage.loadGraph();
420
- // Count entity types
421
- const entityTypes = {};
422
- graph.entities.forEach(e => {
423
- entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
424
- });
425
- // Calculate averages
426
- const avgConfidence = graph.entities.length > 0
427
- ? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
428
- : 0;
429
- const avgImportance = graph.entities.length > 0
430
- ? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
431
- : 0;
432
- // Count unique threads
433
- const threads = new Set([
434
- ...graph.entities.map(e => e.agentThreadId),
435
- ...graph.relations.map(r => r.agentThreadId)
436
- ]);
437
- // Recent activity (last 7 days, grouped by day)
438
- const now = new Date();
439
- const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
440
- const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
441
- // Group by day
442
- const activityByDay = {};
443
- recentEntities.forEach(e => {
444
- const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
445
- activityByDay[day] = (activityByDay[day] || 0) + 1;
446
- });
447
- const recentActivity = Object.entries(activityByDay)
448
- .map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
449
- .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
450
- return {
451
- entityCount: graph.entities.length,
452
- relationCount: graph.relations.length,
453
- threadCount: threads.size,
454
- entityTypes,
455
- avgConfidence,
456
- avgImportance,
457
- recentActivity
458
- };
109
+ await this.ensureInitialized();
110
+ return MemoryStats.getMemoryStats(this.storage);
459
111
  }
460
- // Enhancement 2: Get recent changes
461
112
  async getRecentChanges(since) {
462
- const graph = await this.storage.loadGraph();
463
- const sinceDate = new Date(since);
464
- // Only return entities and relations that were actually modified since the specified time
465
- const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
466
- // Only include relations that are recent themselves
467
- const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
468
- return {
469
- entities: recentEntities,
470
- relations: recentRelations
471
- };
113
+ await this.ensureInitialized();
114
+ return MemoryStats.getRecentChanges(this.storage, since);
472
115
  }
473
- // Enhancement 3: Relationship path finding
116
+ // Analysis Operations
474
117
  async findRelationPath(from, to, maxDepth = 5) {
475
- const graph = await this.storage.loadGraph();
476
- if (from === to) {
477
- return { found: true, path: [from], relations: [] };
478
- }
479
- // Build indexes for efficient relation lookup
480
- const relationsFrom = new Map();
481
- const relationsTo = new Map();
482
- for (const rel of graph.relations) {
483
- if (!relationsFrom.has(rel.from)) {
484
- relationsFrom.set(rel.from, []);
485
- }
486
- relationsFrom.get(rel.from).push(rel);
487
- if (!relationsTo.has(rel.to)) {
488
- relationsTo.set(rel.to, []);
489
- }
490
- relationsTo.get(rel.to).push(rel);
491
- }
492
- // BFS to find shortest path
493
- const queue = [
494
- { entity: from, path: [from], relations: [] }
495
- ];
496
- const visited = new Set([from]);
497
- while (queue.length > 0) {
498
- const current = queue.shift();
499
- if (current.path.length > maxDepth) {
500
- continue;
501
- }
502
- // Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
503
- const outgoing = relationsFrom.get(current.entity) || [];
504
- const incoming = relationsTo.get(current.entity) || [];
505
- // Check outgoing relations
506
- for (const rel of outgoing) {
507
- if (rel.to === to) {
508
- return {
509
- found: true,
510
- path: [...current.path, rel.to],
511
- relations: [...current.relations, rel]
512
- };
513
- }
514
- if (!visited.has(rel.to)) {
515
- visited.add(rel.to);
516
- queue.push({
517
- entity: rel.to,
518
- path: [...current.path, rel.to],
519
- relations: [...current.relations, rel]
520
- });
521
- }
522
- }
523
- // Check incoming relations (traverse backwards)
524
- for (const rel of incoming) {
525
- if (rel.from === to) {
526
- return {
527
- found: true,
528
- path: [...current.path, rel.from],
529
- relations: [...current.relations, rel]
530
- };
531
- }
532
- if (!visited.has(rel.from)) {
533
- visited.add(rel.from);
534
- queue.push({
535
- entity: rel.from,
536
- path: [...current.path, rel.from],
537
- relations: [...current.relations, rel]
538
- });
539
- }
540
- }
541
- }
542
- return { found: false, path: [], relations: [] };
118
+ await this.ensureInitialized();
119
+ return PathFinder.findRelationPath(this.storage, from, to, maxDepth);
543
120
  }
544
- // Enhancement 4: Detect conflicting observations
545
121
  async detectConflicts() {
546
- const graph = await this.storage.loadGraph();
547
- const conflicts = [];
548
- for (const entity of graph.entities) {
549
- const entityConflicts = [];
550
- for (let i = 0; i < entity.observations.length; i++) {
551
- for (let j = i + 1; j < entity.observations.length; j++) {
552
- const obs1Content = entity.observations[i].content.toLowerCase();
553
- const obs2Content = entity.observations[j].content.toLowerCase();
554
- // Skip if observations are in the same version chain
555
- if (entity.observations[i].supersedes === entity.observations[j].id ||
556
- entity.observations[j].supersedes === entity.observations[i].id ||
557
- entity.observations[i].superseded_by === entity.observations[j].id ||
558
- entity.observations[j].superseded_by === entity.observations[i].id) {
559
- continue;
560
- }
561
- // Check for negation patterns
562
- const obs1HasNegation = this.hasNegation(obs1Content);
563
- const obs2HasNegation = this.hasNegation(obs2Content);
564
- // If one has negation and they share key words, might be a conflict
565
- if (obs1HasNegation !== obs2HasNegation) {
566
- const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
567
- const words2Set = new Set(obs2Content.split(/\s+/).filter(w => w.length > 3));
568
- const commonWords = words1.filter(w => words2Set.has(w) && !KnowledgeGraphManager.NEGATION_WORDS.has(w));
569
- if (commonWords.length >= 2) {
570
- entityConflicts.push({
571
- obs1: entity.observations[i].content,
572
- obs2: entity.observations[j].content,
573
- reason: 'Potential contradiction with negation'
574
- });
575
- }
576
- }
577
- }
578
- }
579
- if (entityConflicts.length > 0) {
580
- conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
581
- }
582
- }
583
- return conflicts;
122
+ await this.ensureInitialized();
123
+ return ConflictDetector.detectConflicts(this.storage);
124
+ }
125
+ async getContext(entityNames, depth = 1) {
126
+ await this.ensureInitialized();
127
+ return ContextBuilder.getContext(this.storage, entityNames, depth);
128
+ }
129
+ async getAnalytics(threadId) {
130
+ await this.ensureInitialized();
131
+ return AnalyticsService.getAnalytics(this.storage, threadId);
584
132
  }
585
- // Enhancement 5: Memory pruning
133
+ // Memory Maintenance
586
134
  async pruneMemory(options) {
587
- const graph = await this.storage.loadGraph();
588
- const initialEntityCount = graph.entities.length;
589
- const initialRelationCount = graph.relations.length;
590
- // Filter entities to remove
591
- let entitiesToKeep = graph.entities;
592
- if (options.olderThan) {
593
- const cutoffDate = new Date(options.olderThan);
594
- entitiesToKeep = entitiesToKeep.filter(e => new Date(e.timestamp) >= cutoffDate);
595
- }
596
- if (options.importanceLessThan !== undefined) {
597
- entitiesToKeep = entitiesToKeep.filter(e => e.importance >= options.importanceLessThan);
598
- }
599
- // Ensure we keep minimum entities
600
- // If keepMinEntities is set and we need more entities, take from the already-filtered set
601
- // sorted by importance and recency
602
- if (options.keepMinEntities && entitiesToKeep.length < options.keepMinEntities) {
603
- // Sort the filtered entities by importance and timestamp, keep the most important and recent
604
- const sorted = [...entitiesToKeep].sort((a, b) => {
605
- if (a.importance !== b.importance)
606
- return b.importance - a.importance;
607
- return b.timestamp.localeCompare(a.timestamp);
608
- });
609
- // If we still don't have enough, we keep what we have
610
- entitiesToKeep = sorted.slice(0, Math.min(options.keepMinEntities, sorted.length));
611
- }
612
- const keptEntityNames = new Set(entitiesToKeep.map(e => e.name));
613
- // Remove relations that reference removed entities
614
- const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
615
- graph.entities = entitiesToKeep;
616
- graph.relations = relationsToKeep;
617
- await this.storage.saveGraph(graph);
618
- return {
619
- removedEntities: initialEntityCount - entitiesToKeep.length,
620
- removedRelations: initialRelationCount - relationsToKeep.length
621
- };
135
+ await this.ensureInitialized();
136
+ return MemoryPruner.pruneMemory(this.storage, options);
622
137
  }
623
- // Enhancement 6: Batch operations
624
138
  async bulkUpdate(updates) {
625
- const graph = await this.storage.loadGraph();
626
- let updated = 0;
627
- const notFound = [];
628
- for (const update of updates) {
629
- const entity = graph.entities.find(e => e.name === update.entityName);
630
- if (!entity) {
631
- notFound.push(update.entityName);
632
- continue;
633
- }
634
- if (update.confidence !== undefined) {
635
- entity.confidence = update.confidence;
636
- }
637
- if (update.importance !== undefined) {
638
- entity.importance = update.importance;
639
- }
640
- if (update.addObservations) {
641
- // Filter out observations that already exist (by content)
642
- const newObsContents = update.addObservations.filter(obsContent => !entity.observations.some(o => o.content === obsContent));
643
- // Create Observation objects for new observations
644
- const newObservations = newObsContents.map(content => ({
645
- id: `obs_${randomUUID()}`,
646
- content: content,
647
- timestamp: new Date().toISOString(),
648
- version: 1,
649
- agentThreadId: entity.agentThreadId, // Use entity's thread ID
650
- confidence: update.confidence ?? entity.confidence,
651
- importance: update.importance ?? entity.importance
652
- }));
653
- entity.observations.push(...newObservations);
654
- }
655
- entity.timestamp = new Date().toISOString();
656
- updated++;
657
- }
658
- await this.storage.saveGraph(graph);
659
- return { updated, notFound };
139
+ await this.ensureInitialized();
140
+ return BulkUpdater.bulkUpdate(this.storage, updates);
660
141
  }
661
- // Enhancement 7: Flag for review (Human-in-the-Loop)
142
+ // Collaboration Features
662
143
  async flagForReview(entityName, reason, reviewer) {
663
- const graph = await this.storage.loadGraph();
664
- const entity = graph.entities.find(e => e.name === entityName);
665
- if (!entity) {
666
- throw new Error(`Entity with name ${entityName} not found`);
667
- }
668
- // Add a special observation to mark for review
669
- const flagContent = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
670
- // Check if this flag already exists (by content)
671
- if (!entity.observations.some(o => o.content === flagContent)) {
672
- const flagObservation = {
673
- id: `obs_${randomUUID()}`,
674
- content: flagContent,
675
- timestamp: new Date().toISOString(),
676
- version: 1,
677
- agentThreadId: entity.agentThreadId,
678
- confidence: 1.0, // Flag observations have full confidence
679
- importance: 1.0 // Flag observations are highly important
680
- };
681
- entity.observations.push(flagObservation);
682
- entity.timestamp = new Date().toISOString();
683
- await this.storage.saveGraph(graph);
684
- }
144
+ await this.ensureInitialized();
145
+ return FlagManager.flagForReview(this.storage, entityName, reason, reviewer);
685
146
  }
686
- // Enhancement 8: Get entities flagged for review
687
147
  async getFlaggedEntities() {
688
- const graph = await this.storage.loadGraph();
689
- return graph.entities.filter(e => e.observations.some(obs => obs.content.includes('[FLAGGED FOR REVIEW:')));
690
- }
691
- // Enhancement 9: Get context (entities related to a topic/entity)
692
- async getContext(entityNames, depth = 1) {
693
- const graph = await this.storage.loadGraph();
694
- const contextEntityNames = new Set(entityNames);
695
- // Expand to include related entities up to specified depth
696
- for (let d = 0; d < depth; d++) {
697
- const currentEntities = Array.from(contextEntityNames);
698
- for (const entityName of currentEntities) {
699
- // Find all relations involving this entity
700
- const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
701
- // Add related entities
702
- relatedRelations.forEach(r => {
703
- contextEntityNames.add(r.from);
704
- contextEntityNames.add(r.to);
705
- });
706
- }
707
- }
708
- // Get all entities and relations in context
709
- const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
710
- const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
711
- return {
712
- entities: contextEntities,
713
- relations: contextRelations
714
- };
148
+ await this.ensureInitialized();
149
+ return FlagManager.getFlaggedEntities(this.storage);
715
150
  }
716
- // Enhancement 10: List conversations (agent threads)
717
151
  async listConversations() {
718
- const graph = await this.storage.loadGraph();
719
- // Group data by agent thread
720
- const threadMap = new Map();
721
- // Collect entities by thread
722
- for (const entity of graph.entities) {
723
- if (!threadMap.has(entity.agentThreadId)) {
724
- threadMap.set(entity.agentThreadId, { entities: [], relations: [], timestamps: [] });
725
- }
726
- const threadData = threadMap.get(entity.agentThreadId);
727
- threadData.entities.push(entity);
728
- threadData.timestamps.push(entity.timestamp);
729
- }
730
- // Collect relations by thread
731
- for (const relation of graph.relations) {
732
- if (!threadMap.has(relation.agentThreadId)) {
733
- threadMap.set(relation.agentThreadId, { entities: [], relations: [], timestamps: [] });
734
- }
735
- const threadData = threadMap.get(relation.agentThreadId);
736
- threadData.relations.push(relation);
737
- threadData.timestamps.push(relation.timestamp);
738
- }
739
- // Build conversation summaries
740
- const conversations = Array.from(threadMap.entries()).map(([agentThreadId, data]) => {
741
- const timestamps = data.timestamps.sort((a, b) => a.localeCompare(b));
742
- return {
743
- agentThreadId,
744
- entityCount: data.entities.length,
745
- relationCount: data.relations.length,
746
- firstCreated: timestamps[0] || '',
747
- lastUpdated: timestamps[timestamps.length - 1] || ''
748
- };
749
- });
750
- // Sort by last updated (most recent first)
751
- conversations.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
752
- return { conversations };
753
- }
754
- // Analytics: Get analytics for a specific thread (limited to 4 core metrics)
755
- async getAnalytics(threadId) {
756
- const graph = await this.storage.loadGraph();
757
- // Filter to thread-specific data
758
- const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
759
- const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
760
- // 1. Recent changes (last 10, sorted by timestamp)
761
- const recent_changes = threadEntities
762
- .map(e => ({
763
- entityName: e.name,
764
- entityType: e.entityType,
765
- lastModified: e.timestamp,
766
- changeType: 'created' // Simplified: all are 'created' for now
767
- }))
768
- .sort((a, b) => b.lastModified.localeCompare(a.lastModified))
769
- .slice(0, 10);
770
- // 2. Top by importance (top 10)
771
- const top_important = threadEntities
772
- .map(e => ({
773
- entityName: e.name,
774
- entityType: e.entityType,
775
- importance: e.importance,
776
- observationCount: e.observations.length
777
- }))
778
- .sort((a, b) => b.importance - a.importance)
779
- .slice(0, 10);
780
- // 3. Most connected entities (top 10 by relation count)
781
- const entityRelationCounts = new Map();
782
- for (const entity of threadEntities) {
783
- entityRelationCounts.set(entity.name, new Set());
784
- }
785
- for (const relation of threadRelations) {
786
- if (entityRelationCounts.has(relation.from)) {
787
- entityRelationCounts.get(relation.from).add(relation.to);
788
- }
789
- if (entityRelationCounts.has(relation.to)) {
790
- entityRelationCounts.get(relation.to).add(relation.from);
791
- }
792
- }
793
- const most_connected = Array.from(entityRelationCounts.entries())
794
- .map(([entityName, connectedSet]) => {
795
- const entity = threadEntities.find(e => e.name === entityName);
796
- return {
797
- entityName,
798
- entityType: entity.entityType,
799
- relationCount: connectedSet.size,
800
- connectedTo: Array.from(connectedSet)
801
- };
802
- })
803
- .sort((a, b) => b.relationCount - a.relationCount)
804
- .slice(0, 10);
805
- // 4. Orphaned entities (entities with no relations or broken relations)
806
- const orphaned_entities = [];
807
- const allEntityNames = new Set(threadEntities.map(e => e.name));
808
- for (const entity of threadEntities) {
809
- const relationCount = entityRelationCounts.get(entity.name)?.size || 0;
810
- if (relationCount === 0) {
811
- orphaned_entities.push({
812
- entityName: entity.name,
813
- entityType: entity.entityType,
814
- reason: 'no_relations'
815
- });
816
- }
817
- else {
818
- // Check for broken relations (pointing to non-existent entities)
819
- const entityRelations = threadRelations.filter(r => r.from === entity.name || r.to === entity.name);
820
- const hasBrokenRelation = entityRelations.some(r => !allEntityNames.has(r.from) || !allEntityNames.has(r.to));
821
- if (hasBrokenRelation) {
822
- orphaned_entities.push({
823
- entityName: entity.name,
824
- entityType: entity.entityType,
825
- reason: 'broken_relation'
826
- });
827
- }
828
- }
829
- }
830
- return {
831
- recent_changes,
832
- top_important,
833
- most_connected,
834
- orphaned_entities
835
- };
152
+ await this.ensureInitialized();
153
+ return ConversationService.listConversations(this.storage);
836
154
  }
837
- // Observation Versioning: Get full history chain for an observation
155
+ // Observation Versioning
838
156
  async getObservationHistory(entityName, observationId) {
839
- const graph = await this.storage.loadGraph();
840
- // Find the entity
841
- const entity = graph.entities.find(e => e.name === entityName);
842
- if (!entity) {
843
- throw new Error(`Entity '${entityName}' not found`);
844
- }
845
- // Find the starting observation
846
- const startObs = entity.observations.find(o => o.id === observationId);
847
- if (!startObs) {
848
- throw new Error(`Observation '${observationId}' not found in entity '${entityName}'`);
849
- }
850
- // Build the version chain
851
- const history = [];
852
- // Trace backwards to find all predecessors
853
- let currentObs = startObs;
854
- const visited = new Set();
855
- while (currentObs) {
856
- if (visited.has(currentObs.id)) {
857
- // Circular reference protection
858
- break;
859
- }
860
- visited.add(currentObs.id);
861
- history.unshift(currentObs); // Add to beginning for chronological order
862
- // Find predecessor
863
- if (currentObs.supersedes) {
864
- currentObs = entity.observations.find(o => o.id === currentObs.supersedes);
865
- }
866
- else {
867
- break;
868
- }
869
- }
870
- // Trace forwards to find all successors
871
- let forwardObs = startObs;
872
- visited.clear();
873
- while (forwardObs.superseded_by) {
874
- if (visited.has(forwardObs.superseded_by)) {
875
- // Circular reference protection
876
- break;
877
- }
878
- const successor = entity.observations.find(o => o.id === forwardObs.superseded_by);
879
- if (successor) {
880
- visited.add(successor.id);
881
- history.push(successor);
882
- forwardObs = successor;
883
- }
884
- else {
885
- break;
886
- }
887
- }
888
- return history;
157
+ await this.ensureInitialized();
158
+ return ObservationHistory.getObservationHistory(this.storage, entityName, observationId);
889
159
  }
890
160
  }