server-memory-enhanced 2.3.0 → 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.
Files changed (32) hide show
  1. package/README.md +245 -44
  2. package/dist/index.js +27 -1
  3. package/dist/lib/analysis/analytics-service.js +111 -0
  4. package/dist/lib/analysis/conflict-detector.js +48 -0
  5. package/dist/lib/analysis/context-builder.js +31 -0
  6. package/dist/lib/analysis/memory-stats.js +63 -0
  7. package/dist/lib/analysis/path-finder.js +77 -0
  8. package/dist/lib/collaboration/conversation-service.js +43 -0
  9. package/dist/lib/collaboration/flag-manager.js +38 -0
  10. package/dist/lib/jsonl-storage-adapter.js +8 -4
  11. package/dist/lib/knowledge-graph-manager.js +94 -663
  12. package/dist/lib/maintenance/bulk-updater.js +44 -0
  13. package/dist/lib/maintenance/memory-pruner.js +48 -0
  14. package/dist/lib/operations/entity-operations.js +27 -0
  15. package/dist/lib/operations/observation-operations.js +105 -0
  16. package/dist/lib/operations/relation-operations.js +45 -0
  17. package/dist/lib/queries/entity-queries.js +68 -0
  18. package/dist/lib/queries/graph-reader.js +9 -0
  19. package/dist/lib/queries/search-service.js +99 -0
  20. package/dist/lib/schemas.js +15 -0
  21. package/dist/lib/utils/entity-finder.js +31 -0
  22. package/dist/lib/utils/negation-detector.js +23 -0
  23. package/dist/lib/utils/observation-validator.js +43 -0
  24. package/dist/lib/utils/relation-key.js +16 -0
  25. package/dist/lib/validation/entity-type-validator.js +32 -0
  26. package/dist/lib/validation/observation-validator.js +61 -0
  27. package/dist/lib/validation/quality-scorer.js +27 -0
  28. package/dist/lib/validation/relation-validator.js +36 -0
  29. package/dist/lib/validation/request-validator.js +91 -0
  30. package/dist/lib/validation.js +8 -200
  31. package/dist/lib/versioning/observation-history.js +54 -0
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -15,8 +15,10 @@ An enhanced version of the Memory MCP server that provides persistent knowledge
15
15
  ### Entities
16
16
  Each entity now includes:
17
17
  - `name`: Entity identifier
18
- - `entityType`: Type of entity
19
- - `observations`: Array of observation strings
18
+ - `entityType`: Type of entity (free-form, any domain-specific type allowed)
19
+ - `observations`: Array of **versioned Observation objects** (not strings) - **BREAKING CHANGE**
20
+ - Each observation has: `id`, `content`, `timestamp`, `version`, `supersedes`, `superseded_by`
21
+ - Supports full version history tracking
20
22
  - `agentThreadId`: Unique identifier for the agent thread
21
23
  - `timestamp`: ISO 8601 timestamp of creation
22
24
  - `confidence`: Confidence score (0.0 to 1.0)
@@ -52,39 +54,64 @@ The server stores data in separate JSONL files per agent thread:
52
54
 
53
55
  ## Available Tools
54
56
 
57
+ ### ⭐ Recommended Tool (New)
58
+ 1. **save_memory**: **[RECOMMENDED]** Unified tool for creating entities and relations atomically with server-side validation
59
+ - Enforces observation limits (max 300 chars, 3 sentences per observation, ignoring periods in version numbers)
60
+ - Requires at least 1 relation per entity (prevents orphaned nodes)
61
+ - Free-form entity types with soft normalization
62
+ - Atomic transactions (all-or-nothing)
63
+ - Bidirectional relation tracking
64
+ - Quality score calculation
65
+ - Clear, actionable error messages
66
+
55
67
  ### Core Operations
56
- 1. **create_entities**: Create new entities with metadata (including importance)
57
- 2. **create_relations**: Create relationships between entities with metadata (including importance)
68
+ > ⚠️ **Note**: `create_entities` and `create_relations` are **deprecated**. New code should use `save_memory` for better reliability and validation.
69
+
70
+ 1. **create_entities**: Create new entities with metadata (including importance) - **[DEPRECATED - Use save_memory]**
71
+ 2. **create_relations**: Create relationships between entities with metadata (including importance) - **[DEPRECATED - Use save_memory]**
58
72
  3. **add_observations**: Add observations to existing entities with metadata (including importance)
59
- 4. **delete_entities**: Remove entities and cascading relations
60
- 5. **delete_observations**: Remove specific observations
61
- 6. **delete_relations**: Delete relationships
62
- 7. **read_graph**: Read the entire knowledge graph
63
- 8. **search_nodes**: Search entities by name, type, or observation content
64
- 9. **open_nodes**: Retrieve specific entities by name
65
- 10. **query_nodes**: Advanced querying with range-based filtering by timestamp, confidence, and importance
73
+ 4. **update_observation**: **[NEW]** Update an existing observation by creating a new version with updated content, maintaining version history
74
+ 5. **delete_entities**: Remove entities and cascading relations
75
+ 6. **delete_observations**: Remove specific observations
76
+ 7. **delete_relations**: Delete relationships
77
+ 8. **read_graph**: Read the entire knowledge graph
78
+ 9. **search_nodes**: Search entities by name, type, or observation content
79
+ 10. **open_nodes**: Retrieve specific entities by name
80
+ 11. **query_nodes**: Advanced querying with range-based filtering by timestamp, confidence, and importance
81
+ 12. **list_entities**: List entities with optional filtering by type and name pattern for quick discovery
82
+ 13. **validate_memory**: Validate entities without saving (dry-run) - check for errors before attempting save_memory
66
83
 
67
84
  ### Memory Management & Insights
68
- 11. **get_memory_stats**: Get comprehensive statistics (entity counts, thread activity, avg confidence/importance, recent activity)
69
- 12. **get_recent_changes**: Retrieve entities and relations created/modified since a specific timestamp
70
- 13. **prune_memory**: Remove old or low-importance entities to manage memory size
71
- 14. **bulk_update**: Efficiently update multiple entities at once (confidence, importance, observations)
85
+ 14. **get_analytics**: **[NEW]** Get simple, LLM-friendly analytics about your knowledge graph
86
+ - Recent changes (last 10 entities)
87
+ - Top important entities (by importance score)
88
+ - Most connected entities (by relation count)
89
+ - Orphaned entities (quality check)
90
+ 15. **get_observation_history**: **[NEW]** Retrieve version history for observations
91
+ - Track how observations evolve over time
92
+ - View complete version chains
93
+ - Supports rollback by viewing previous versions
94
+ 16. **get_memory_stats**: Get comprehensive statistics (entity counts, thread activity, avg confidence/importance, recent activity)
95
+ 17. **get_recent_changes**: Retrieve entities and relations created/modified since a specific timestamp
96
+ 18. **prune_memory**: Remove old or low-importance entities to manage memory size
97
+ 19. **bulk_update**: Efficiently update multiple entities at once (confidence, importance, observations)
98
+ 20. **list_conversations**: List all available agent threads (conversations) with metadata including entity counts, relation counts, and activity timestamps
72
99
 
73
100
  ### Relationship Intelligence
74
- 15. **find_relation_path**: Find the shortest path of relationships between two entities (useful for "how are they connected?")
75
- 16. **get_context**: Retrieve entities and relations related to specified entities up to a certain depth
101
+ 21. **find_relation_path**: Find the shortest path of relationships between two entities (useful for "how are they connected?")
102
+ 22. **get_context**: Retrieve entities and relations related to specified entities up to a certain depth
76
103
 
77
104
  ### Quality & Review
78
- 17. **detect_conflicts**: Detect potentially conflicting observations using pattern matching and negation detection
79
- 18. **flag_for_review**: Mark entities for human review with a specific reason (Human-in-the-Loop)
80
- 19. **get_flagged_entities**: Retrieve all entities flagged for review
105
+ 23. **detect_conflicts**: Detect potentially conflicting observations using pattern matching and negation detection
106
+ 24. **flag_for_review**: Mark entities for human review with a specific reason (Human-in-the-Loop)
107
+ 25. **get_flagged_entities**: Retrieve all entities flagged for review
81
108
 
82
109
  ## Usage
83
110
 
84
111
  ### Installation
85
112
 
86
113
  ```bash
87
- npm install @modelcontextprotocol/server-memory-enhanced
114
+ npm install server-memory-enhanced
88
115
  ```
89
116
 
90
117
  ### Running the Server
@@ -95,44 +122,150 @@ npx mcp-server-memory-enhanced
95
122
 
96
123
  ### Configuration
97
124
 
125
+ #### File Storage (Default)
126
+
98
127
  Set the `MEMORY_DIR_PATH` environment variable to customize the storage location:
99
128
 
100
129
  ```bash
101
130
  MEMORY_DIR_PATH=/path/to/memory/directory npx mcp-server-memory-enhanced
102
131
  ```
103
132
 
104
- ## Example
133
+ #### Neo4j Storage (Optional)
134
+
135
+ The server supports Neo4j as an alternative storage backend. If Neo4j environment variables are set, the server will attempt to connect to Neo4j. If the connection fails or variables are not set, it will automatically fall back to file-based JSONL storage.
136
+
137
+ **Environment Variables:**
138
+
139
+ ```bash
140
+ # Neo4j connection settings
141
+ export NEO4J_URI=neo4j://localhost:7687
142
+ export NEO4J_USERNAME=neo4j
143
+ export NEO4J_PASSWORD=your_password
144
+ export NEO4J_DATABASE=neo4j # Optional, defaults to 'neo4j'
145
+
146
+ # Run the server
147
+ npx mcp-server-memory-enhanced
148
+ ```
149
+
150
+ **Using Docker Compose:**
151
+
152
+ A `docker-compose.yml` file is provided for local development with Neo4j:
153
+
154
+ ```bash
155
+ # Start Neo4j and the MCP server
156
+ docker-compose up
157
+
158
+ # The Neo4j browser will be available at http://localhost:7474
159
+ # Username: neo4j, Password: testpassword
160
+ ```
161
+
162
+ **Using with Claude Desktop:**
163
+
164
+ Configure the server in your Claude Desktop configuration with Neo4j:
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "memory-enhanced": {
170
+ "command": "npx",
171
+ "args": ["-y", "mcp-server-memory-enhanced"],
172
+ "env": {
173
+ "NEO4J_URI": "neo4j://localhost:7687",
174
+ "NEO4J_USERNAME": "neo4j",
175
+ "NEO4J_PASSWORD": "your_password"
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ **Benefits of Neo4j Storage:**
183
+
184
+ - **Graph-native queries**: Faster relationship traversals and path finding
185
+ - **Scalability**: Better performance with large knowledge graphs
186
+ - **Advanced queries**: Native support for graph algorithms
187
+ - **Visualization**: Use Neo4j Browser to visualize your knowledge graph
188
+ - **Automatic fallback**: If Neo4j is not available, automatically uses file storage
189
+
190
+ ## User Guide
191
+
192
+ ### ✨ Using save_memory (Recommended)
193
+
194
+ The `save_memory` tool is the recommended way to create entities and relations. It provides atomic transactions and server-side validation to ensure high-quality knowledge graphs.
195
+
196
+ #### Key Principles
197
+
198
+ 1. **Atomic Observations**: Each observation should be a single, atomic fact
199
+ - ✅ Good: `"Works at Google"`, `"Lives in San Francisco"`
200
+ - ❌ Bad: `"Works at Google and lives in San Francisco and has a PhD in Computer Science"`
201
+ - **Max length**: 300 characters per observation
202
+ - **Max sentences**: 3 sentences per observation (technical content with version numbers supported)
203
+
204
+ 2. **Mandatory Relations**: Every entity must connect to at least one other entity
205
+ - ✅ Good: `{ targetEntity: "Google", relationType: "works at" }`
206
+ - ❌ Bad: Empty relations array `[]`
207
+ - This prevents orphaned nodes and ensures a well-connected knowledge graph
208
+
209
+ 3. **Free Entity Types**: Use any entity type that makes sense for your domain
210
+ - ✅ Good: `"Person"`, `"Company"`, `"Document"`, `"Recipe"`, `"Patient"`, `"API"`
211
+ - Soft normalization: `"person"` → `"Person"` (warning, not error)
212
+ - Space warning: `"API Key"` → suggests `"APIKey"`
213
+
214
+ 4. **Error Messages**: The tool provides clear, actionable error messages
215
+ - Too long: `"Observation too long (350 chars). Max 300. Suggestion: Split into multiple observations."`
216
+ - No relations: `"Entity 'X' must have at least 1 relation. Suggestion: Add relations to show connections."`
217
+ - Too many sentences: `"Too many sentences (4). Max 3. Suggestion: Split this into 4 separate observations."`
218
+
219
+ ### Example Usage
105
220
 
106
221
  ```typescript
107
- // Create entities with metadata including importance
108
- await createEntities({
222
+ // RECOMMENDED: Use save_memory for atomic entity and relation creation
223
+ await save_memory({
109
224
  entities: [
110
225
  {
111
226
  name: "Alice",
112
- entityType: "person",
113
- observations: ["works at Acme Corp"],
114
- agentThreadId: "thread-001",
115
- timestamp: "2024-01-20T10:00:00Z",
116
- confidence: 0.95,
117
- importance: 0.9 // Critical entity
227
+ entityType: "Person",
228
+ observations: ["Works at Google", "Lives in SF"], // Atomic facts, under 300 chars
229
+ relations: [{ targetEntity: "Bob", relationType: "knows" }] // At least 1 relation required
230
+ },
231
+ {
232
+ name: "Bob",
233
+ entityType: "Person",
234
+ observations: ["Works at Microsoft"],
235
+ relations: [{ targetEntity: "Alice", relationType: "knows" }]
118
236
  }
119
- ]
237
+ ],
238
+ threadId: "conversation-001"
120
239
  });
121
240
 
122
- // Create relations with metadata including importance
123
- await createRelations({
124
- relations: [
125
- {
126
- from: "Alice",
127
- to: "Bob",
128
- relationType: "knows",
129
- agentThreadId: "thread-001",
130
- timestamp: "2024-01-20T10:01:00Z",
131
- confidence: 0.9,
132
- importance: 0.75 // Important relationship
133
- }
134
- ]
241
+ // Get analytics about your knowledge graph
242
+ await get_analytics({
243
+ threadId: "conversation-001"
244
+ });
245
+ // Returns: {
246
+ // recent_changes: [...], // Last 10 entities
247
+ // top_important: [...], // Top 10 by importance
248
+ // most_connected: [...], // Top 10 by relation count
249
+ // orphaned_entities: [...] // Quality check
250
+ // }
251
+
252
+ // Get observation version history
253
+ await get_observation_history({
254
+ entityName: "Python Scripts",
255
+ observationId: "obs_abc123"
135
256
  });
257
+ // Returns: { history: [{ id, content, version, timestamp, supersedes, superseded_by }, ...] }
258
+
259
+ // Update an existing observation (creates a new version)
260
+ await update_observation({
261
+ entityName: "Alice",
262
+ observationId: "obs_abc123",
263
+ newContent: "Works at Google (Senior Engineer)",
264
+ agentThreadId: "conversation-001",
265
+ timestamp: "2024-01-20T12:00:00Z",
266
+ confidence: 0.95 // Optional: update confidence
267
+ });
268
+ // Returns: { success: true, updatedObservation: { id, content, version: 2, supersedes, ... }, message: "..." }
136
269
 
137
270
  // Query nodes with range-based filtering
138
271
  await queryNodes({
@@ -146,6 +279,10 @@ await queryNodes({
146
279
  await getMemoryStats();
147
280
  // Returns: { entityCount, relationCount, threadCount, entityTypes, avgConfidence, avgImportance, recentActivity }
148
281
 
282
+ // List all conversations (agent threads)
283
+ await listConversations();
284
+ // Returns: { conversations: [{ agentThreadId, entityCount, relationCount, firstCreated, lastUpdated }, ...] }
285
+
149
286
  // Get recent changes since last interaction
150
287
  await getRecentChanges({ since: "2024-01-20T10:00:00Z" });
151
288
 
@@ -173,6 +310,49 @@ await bulkUpdate({
173
310
  await pruneMemory({ olderThan: "2024-01-01T00:00:00Z", importanceLessThan: 0.3, keepMinEntities: 100 });
174
311
  ```
175
312
 
313
+ ### 🔄 Migration Guide
314
+
315
+ For users of the old `create_entities` and `create_relations` tools:
316
+
317
+ #### What Changed
318
+ - **Old approach**: Two separate tools that could be used independently
319
+ - `create_entities` → creates entities
320
+ - `create_relations` → creates relations (optional, often skipped by LLMs)
321
+ - **New approach**: Single `save_memory` tool with atomic transactions
322
+ - Creates entities and relations together
323
+ - Enforces mandatory relations (at least 1 per entity)
324
+ - Validates observation length and atomicity
325
+
326
+ #### Migrating Your Code
327
+ ```typescript
328
+ // ❌ OLD WAY (deprecated but still works)
329
+ await create_entities({
330
+ entities: [{ name: "Alice", entityType: "person", observations: ["works at Google and lives in SF"] }]
331
+ });
332
+ await create_relations({ // Often forgotten!
333
+ relations: [{ from: "Alice", to: "Bob", relationType: "knows" }]
334
+ });
335
+
336
+ // ✅ NEW WAY (recommended)
337
+ await save_memory({
338
+ entities: [
339
+ {
340
+ name: "Alice",
341
+ entityType: "Person",
342
+ observations: ["Works at Google", "Lives in SF"], // Split into atomic facts
343
+ relations: [{ targetEntity: "Bob", relationType: "knows" }] // Required!
344
+ }
345
+ ],
346
+ threadId: "conversation-001"
347
+ });
348
+ ```
349
+
350
+ #### Migration Strategy
351
+ 1. **Old tools remain available**: `create_entities` and `create_relations` are deprecated but not removed
352
+ 2. **No forced migration**: Update your code gradually at your own pace
353
+ 3. **New code should use `save_memory`**: Benefits from validation and atomic transactions
354
+ 4. **Observation versioning**: New installations use versioned observations (breaking change for data model)
355
+
176
356
  ## Development
177
357
 
178
358
  ### Build
@@ -196,3 +376,24 @@ npm run watch
196
376
  ## License
197
377
 
198
378
  MIT
379
+
380
+ ## 🤝 Contributing
381
+
382
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
383
+
384
+ ## 🔒 Security
385
+
386
+ See [SECURITY.MD](SECURITY.md) for reporting security vulnerabilities.
387
+
388
+ ## 📜 License
389
+
390
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
391
+
392
+ ## 💬 Community
393
+
394
+ - [GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions)
395
+ - [Model Context Protocol Documentation](https://modelcontextprotocol.io)
396
+
397
+ ---
398
+
399
+ Part of the [Model Context Protocol](https://modelcontextprotocol.io) project.
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 } from './lib/schemas.js';
9
+ import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema } 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';
@@ -267,6 +267,32 @@ server.registerTool("delete_observations", {
267
267
  structuredContent: { success: true, message: "Observations deleted successfully" }
268
268
  };
269
269
  });
270
+ // Register update_observation tool
271
+ server.registerTool("update_observation", {
272
+ title: "Update Observation",
273
+ description: "Update an existing observation by creating a new version with updated content. This maintains version history through the supersedes/superseded_by chain.",
274
+ inputSchema: UpdateObservationInputSchema.shape,
275
+ outputSchema: UpdateObservationOutputSchema.shape
276
+ }, async ({ entityName, observationId, newContent, agentThreadId, timestamp, confidence, importance }) => {
277
+ const updatedObservation = await knowledgeGraphManager.updateObservation({
278
+ entityName,
279
+ observationId,
280
+ newContent,
281
+ agentThreadId,
282
+ timestamp,
283
+ confidence,
284
+ importance
285
+ });
286
+ const message = `Observation updated successfully. New version: ${updatedObservation.id} (v${updatedObservation.version})`;
287
+ return {
288
+ content: [{ type: "text", text: message }],
289
+ structuredContent: {
290
+ success: true,
291
+ updatedObservation,
292
+ message
293
+ }
294
+ };
295
+ });
270
296
  // Register delete_relations tool
271
297
  server.registerTool("delete_relations", {
272
298
  title: "Delete Relations",
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Analytics service for thread-specific metrics
3
+ */
4
+ /**
5
+ * Calculate recent changes for a thread
6
+ */
7
+ function calculateRecentChanges(threadEntities) {
8
+ return threadEntities
9
+ .map(e => ({
10
+ entityName: e.name,
11
+ entityType: e.entityType,
12
+ lastModified: e.timestamp,
13
+ changeType: 'created' // Simplified: all are 'created' for now
14
+ }))
15
+ .sort((a, b) => b.lastModified.localeCompare(a.lastModified))
16
+ .slice(0, 10);
17
+ }
18
+ /**
19
+ * Calculate top important entities for a thread
20
+ */
21
+ function calculateTopImportant(threadEntities) {
22
+ return threadEntities
23
+ .map(e => ({
24
+ entityName: e.name,
25
+ entityType: e.entityType,
26
+ importance: e.importance,
27
+ observationCount: e.observations.length
28
+ }))
29
+ .sort((a, b) => b.importance - a.importance)
30
+ .slice(0, 10);
31
+ }
32
+ /**
33
+ * Calculate most connected entities for a thread
34
+ */
35
+ function calculateMostConnected(threadEntities, entityRelationCounts) {
36
+ return Array.from(entityRelationCounts.entries())
37
+ .map(([entityName, connectedSet]) => {
38
+ const entity = threadEntities.find(e => e.name === entityName);
39
+ return {
40
+ entityName,
41
+ entityType: entity.entityType,
42
+ relationCount: connectedSet.size,
43
+ connectedTo: Array.from(connectedSet)
44
+ };
45
+ })
46
+ .sort((a, b) => b.relationCount - a.relationCount)
47
+ .slice(0, 10);
48
+ }
49
+ /**
50
+ * Calculate orphaned entities for a thread
51
+ */
52
+ function calculateOrphanedEntities(threadEntities, threadRelations, entityRelationCounts) {
53
+ const orphaned_entities = [];
54
+ const allEntityNames = new Set(threadEntities.map(e => e.name));
55
+ for (const entity of threadEntities) {
56
+ const relationCount = entityRelationCounts.get(entity.name)?.size || 0;
57
+ if (relationCount === 0) {
58
+ orphaned_entities.push({
59
+ entityName: entity.name,
60
+ entityType: entity.entityType,
61
+ reason: 'no_relations'
62
+ });
63
+ }
64
+ else {
65
+ // Check for broken relations (pointing to non-existent entities)
66
+ const entityRelations = threadRelations.filter(r => r.from === entity.name || r.to === entity.name);
67
+ const hasBrokenRelation = entityRelations.some(r => !allEntityNames.has(r.from) || !allEntityNames.has(r.to));
68
+ if (hasBrokenRelation) {
69
+ orphaned_entities.push({
70
+ entityName: entity.name,
71
+ entityType: entity.entityType,
72
+ reason: 'broken_relation'
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return orphaned_entities;
78
+ }
79
+ /**
80
+ * Get analytics for a specific thread (limited to 4 core metrics)
81
+ */
82
+ export async function getAnalytics(storage, threadId) {
83
+ const graph = await storage.loadGraph();
84
+ // Filter to thread-specific data
85
+ const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
86
+ const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
87
+ // Calculate all metrics
88
+ const recent_changes = calculateRecentChanges(threadEntities);
89
+ const top_important = calculateTopImportant(threadEntities);
90
+ // Build the relation counts map once for both most_connected and orphaned_entities
91
+ const entityRelationCounts = new Map();
92
+ for (const entity of threadEntities) {
93
+ entityRelationCounts.set(entity.name, new Set());
94
+ }
95
+ for (const relation of threadRelations) {
96
+ if (entityRelationCounts.has(relation.from)) {
97
+ entityRelationCounts.get(relation.from).add(relation.to);
98
+ }
99
+ if (entityRelationCounts.has(relation.to)) {
100
+ entityRelationCounts.get(relation.to).add(relation.from);
101
+ }
102
+ }
103
+ const most_connected = calculateMostConnected(threadEntities, entityRelationCounts);
104
+ const orphaned_entities = calculateOrphanedEntities(threadEntities, threadRelations, entityRelationCounts);
105
+ return {
106
+ recent_changes,
107
+ top_important,
108
+ most_connected,
109
+ orphaned_entities
110
+ };
111
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Conflict detection service
3
+ */
4
+ import { hasNegation, NEGATION_WORDS } from '../utils/negation-detector.js';
5
+ /**
6
+ * Detect conflicting observations within entities
7
+ * Identifies potential contradictions by checking for negation patterns
8
+ */
9
+ export async function detectConflicts(storage) {
10
+ const graph = await storage.loadGraph();
11
+ const conflicts = [];
12
+ for (const entity of graph.entities) {
13
+ const entityConflicts = [];
14
+ for (let i = 0; i < entity.observations.length; i++) {
15
+ for (let j = i + 1; j < entity.observations.length; j++) {
16
+ const obs1Content = entity.observations[i].content.toLowerCase();
17
+ const obs2Content = entity.observations[j].content.toLowerCase();
18
+ // Skip if observations are in the same version chain
19
+ if (entity.observations[i].supersedes === entity.observations[j].id ||
20
+ entity.observations[j].supersedes === entity.observations[i].id ||
21
+ entity.observations[i].superseded_by === entity.observations[j].id ||
22
+ entity.observations[j].superseded_by === entity.observations[i].id) {
23
+ continue;
24
+ }
25
+ // Check for negation patterns
26
+ const obs1HasNegation = hasNegation(obs1Content);
27
+ const obs2HasNegation = hasNegation(obs2Content);
28
+ // If one has negation and they share key words, might be a conflict
29
+ if (obs1HasNegation !== obs2HasNegation) {
30
+ const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
31
+ const words2Set = new Set(obs2Content.split(/\s+/).filter(w => w.length > 3));
32
+ const commonWords = words1.filter(w => words2Set.has(w) && !NEGATION_WORDS.has(w));
33
+ if (commonWords.length >= 2) {
34
+ entityConflicts.push({
35
+ obs1: entity.observations[i].content,
36
+ obs2: entity.observations[j].content,
37
+ reason: 'Potential contradiction with negation'
38
+ });
39
+ }
40
+ }
41
+ }
42
+ }
43
+ if (entityConflicts.length > 0) {
44
+ conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
45
+ }
46
+ }
47
+ return conflicts;
48
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Context builder service
3
+ */
4
+ /**
5
+ * Get context (entities related to specified entities up to a certain depth)
6
+ * Expands to include related entities up to specified depth
7
+ */
8
+ export async function getContext(storage, entityNames, depth = 1) {
9
+ const graph = await storage.loadGraph();
10
+ const contextEntityNames = new Set(entityNames);
11
+ // Expand to include related entities up to specified depth
12
+ for (let d = 0; d < depth; d++) {
13
+ const currentEntities = Array.from(contextEntityNames);
14
+ 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);
17
+ // Add related entities
18
+ relatedRelations.forEach(r => {
19
+ contextEntityNames.add(r.from);
20
+ contextEntityNames.add(r.to);
21
+ });
22
+ }
23
+ }
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));
27
+ return {
28
+ entities: contextEntities,
29
+ relations: contextRelations
30
+ };
31
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Memory statistics service
3
+ */
4
+ /**
5
+ * Get comprehensive memory statistics
6
+ */
7
+ export async function getMemoryStats(storage) {
8
+ const graph = await storage.loadGraph();
9
+ // Count entity types
10
+ const entityTypes = {};
11
+ graph.entities.forEach(e => {
12
+ entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
13
+ });
14
+ // Calculate averages
15
+ const avgConfidence = graph.entities.length > 0
16
+ ? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
17
+ : 0;
18
+ const avgImportance = graph.entities.length > 0
19
+ ? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
20
+ : 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)
27
+ const now = new Date();
28
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
29
+ const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
30
+ // Group by day
31
+ const activityByDay = {};
32
+ recentEntities.forEach(e => {
33
+ const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
34
+ activityByDay[day] = (activityByDay[day] || 0) + 1;
35
+ });
36
+ const recentActivity = Object.entries(activityByDay)
37
+ .map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
38
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
39
+ return {
40
+ entityCount: graph.entities.length,
41
+ relationCount: graph.relations.length,
42
+ threadCount: threads.size,
43
+ entityTypes,
44
+ avgConfidence,
45
+ avgImportance,
46
+ recentActivity
47
+ };
48
+ }
49
+ /**
50
+ * Get recent changes since a specific timestamp
51
+ */
52
+ export async function getRecentChanges(storage, since) {
53
+ const graph = await storage.loadGraph();
54
+ 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
+ return {
60
+ entities: recentEntities,
61
+ relations: recentRelations
62
+ };
63
+ }