server-memory-enhanced 3.1.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
@@ -299,14 +299,15 @@ server.registerTool("delete_relations", {
299
299
  // Register read_graph tool
300
300
  server.registerTool("read_graph", {
301
301
  title: "Read Graph",
302
- 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.",
303
303
  inputSchema: ReadGraphInputSchema,
304
304
  outputSchema: {
305
305
  entities: z.array(EntitySchemaCompat),
306
306
  relations: z.array(RelationSchemaCompat)
307
307
  }
308
308
  }, async (input) => {
309
- const graph = await knowledgeGraphManager.readGraph(input.threadId);
309
+ const { threadId, minImportance } = input;
310
+ const graph = await knowledgeGraphManager.readGraph(threadId, minImportance);
310
311
  return {
311
312
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
312
313
  structuredContent: { ...graph }
@@ -74,8 +74,12 @@ export class KnowledgeGraphManager {
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
@@ -1,18 +1,152 @@
1
1
  /**
2
2
  * Graph reading operations
3
3
  */
4
+ /**
5
+ * Default threshold for marking items as ARCHIVED
6
+ * Items with importance below this but >= minImportance get ARCHIVED status
7
+ */
8
+ export const ARCHIVED_THRESHOLD = 0.1;
9
+ /**
10
+ * Check if an observation has a status field
11
+ */
12
+ function hasStatus(obs) {
13
+ return obs.status !== undefined;
14
+ }
15
+ /**
16
+ * Check if an entity or any of its observations has a status field
17
+ */
18
+ function entityHasStatus(entity) {
19
+ return entity.status !== undefined || entity.observations.some(hasStatus);
20
+ }
21
+ /**
22
+ * Check if a relation has a status field
23
+ */
24
+ function relationHasStatus(relation) {
25
+ return relation.status !== undefined;
26
+ }
27
+ /**
28
+ * Strip status from an observation (used to clean persisted status values)
29
+ */
30
+ function stripObservationStatus(obs) {
31
+ if (!hasStatus(obs))
32
+ return obs;
33
+ const { status: _oldStatus, ...obsWithoutStatus } = obs;
34
+ return obsWithoutStatus;
35
+ }
36
+ /**
37
+ * Strip status from an entity and its observations (used to clean persisted status values)
38
+ * Optimized to avoid unnecessary copies when only entity status needs stripping
39
+ */
40
+ function stripEntityStatus(entity) {
41
+ if (!entityHasStatus(entity))
42
+ return entity;
43
+ const entityHasOwnStatus = entity.status !== undefined;
44
+ const observationsHaveStatus = entity.observations.some(hasStatus);
45
+ // If only observations have status, keep entity as-is and just map observations
46
+ if (!entityHasOwnStatus && observationsHaveStatus) {
47
+ return {
48
+ ...entity,
49
+ observations: entity.observations.map(stripObservationStatus),
50
+ };
51
+ }
52
+ // If only entity has status, strip it but keep observations as-is
53
+ if (entityHasOwnStatus && !observationsHaveStatus) {
54
+ const { status: _oldStatus, ...entityWithoutStatus } = entity;
55
+ return entityWithoutStatus;
56
+ }
57
+ // Both entity and observations have status
58
+ const { status: _oldStatus, ...entityWithoutStatus } = entity;
59
+ return {
60
+ ...entityWithoutStatus,
61
+ observations: entity.observations.map(stripObservationStatus),
62
+ };
63
+ }
64
+ /**
65
+ * Strip status from a relation (used to clean persisted status values)
66
+ */
67
+ function stripRelationStatus(relation) {
68
+ if (!relationHasStatus(relation))
69
+ return relation;
70
+ const { status: _oldStatus, ...relationWithoutStatus } = relation;
71
+ return relationWithoutStatus;
72
+ }
73
+ /**
74
+ * Strip status from all entities and relations in a graph (used to clean persisted status values)
75
+ * Optimized to avoid unnecessary deep copies when no status fields exist
76
+ */
77
+ export function stripGraphStatus(graph) {
78
+ // Check if any items have status fields - if not, return as-is
79
+ const hasAnyStatus = graph.entities.some(entityHasStatus) || graph.relations.some(relationHasStatus);
80
+ if (!hasAnyStatus) {
81
+ return graph;
82
+ }
83
+ return {
84
+ entities: graph.entities.map(stripEntityStatus),
85
+ relations: graph.relations.map(stripRelationStatus),
86
+ };
87
+ }
4
88
  /**
5
89
  * Read the knowledge graph filtered by threadId for thread isolation
90
+ * and optionally filtered by minimum importance threshold
6
91
  */
7
- export async function readGraph(storage, threadId) {
92
+ export async function readGraph(storage, threadId, minImportance = ARCHIVED_THRESHOLD) {
8
93
  const graph = await storage.loadGraph();
9
- // Filter entities by threadId
10
- const filteredEntities = graph.entities.filter(e => e.agentThreadId === threadId);
94
+ // Filter entities by threadId and importance
95
+ const filteredEntities = graph.entities
96
+ .filter(e => e.agentThreadId === threadId)
97
+ .filter(e => e.importance >= minImportance)
98
+ .map(entity => {
99
+ // Add ARCHIVED status if importance is less than ARCHIVED_THRESHOLD but >= minImportance.
100
+ // Otherwise, explicitly clear status so pre-existing values don't leak through.
101
+ const isArchived = entity.importance < ARCHIVED_THRESHOLD && entity.importance >= minImportance;
102
+ const { status: _oldStatus, ...entityWithoutStatus } = entity;
103
+ const entityWithStatus = {
104
+ ...entityWithoutStatus,
105
+ ...(isArchived ? { status: 'ARCHIVED' } : {}),
106
+ };
107
+ // Process observations: filter by importance and add ARCHIVED status
108
+ // Defensive: handle potential legacy string observations
109
+ entityWithStatus.observations = entity.observations
110
+ .filter(obs => {
111
+ // Keep legacy string observations (defensive against legacy data)
112
+ if (typeof obs !== 'object' || obs === null)
113
+ return true;
114
+ // Use observation importance if set, otherwise inherit from entity
115
+ const obsImportance = obs.importance !== undefined ? obs.importance : entity.importance;
116
+ return obsImportance >= minImportance;
117
+ })
118
+ .map(obs => {
119
+ // Pass through legacy string observations unchanged
120
+ if (typeof obs !== 'object' || obs === null)
121
+ return obs;
122
+ const obsImportance = obs.importance !== undefined ? obs.importance : entity.importance;
123
+ const isObsArchived = obsImportance < ARCHIVED_THRESHOLD && obsImportance >= minImportance;
124
+ const { status: _oldObsStatus, ...obsWithoutStatus } = obs;
125
+ return {
126
+ ...obsWithoutStatus,
127
+ ...(isObsArchived ? { status: 'ARCHIVED' } : {}),
128
+ };
129
+ });
130
+ return entityWithStatus;
131
+ });
11
132
  // Create a Set of filtered entity names for quick lookup
12
133
  const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
13
- // Filter relations to only include those between filtered entities and from the same thread
14
- const filteredRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
15
- filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
134
+ // Filter relations to only include those between filtered entities, from the same thread, and by importance
135
+ const filteredRelations = graph.relations
136
+ .filter(r => r.agentThreadId === threadId &&
137
+ filteredEntityNames.has(r.from) &&
138
+ filteredEntityNames.has(r.to) &&
139
+ r.importance >= minImportance)
140
+ .map(relation => {
141
+ // Add ARCHIVED status if importance is less than ARCHIVED_THRESHOLD but >= minImportance.
142
+ // Otherwise, explicitly clear status so pre-existing values don't leak through.
143
+ const isArchived = relation.importance < ARCHIVED_THRESHOLD && relation.importance >= minImportance;
144
+ const { status: _oldStatus, ...relationWithoutStatus } = relation;
145
+ return {
146
+ ...relationWithoutStatus,
147
+ ...(isArchived ? { status: 'ARCHIVED' } : {}),
148
+ };
149
+ });
16
150
  return {
17
151
  entities: filteredEntities,
18
152
  relations: filteredRelations
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Search and filter operations for the knowledge graph
3
3
  */
4
+ import { stripGraphStatus } from './graph-reader.js';
4
5
  /**
5
6
  * Search for nodes in the knowledge graph by query string
6
7
  * Searches entity names, types, and observation content
@@ -22,7 +23,8 @@ export async function searchNodes(storage, threadId, query) {
22
23
  entities: filteredEntities,
23
24
  relations: filteredRelations,
24
25
  };
25
- return filteredGraph;
26
+ // Strip any persisted status values to prevent leaking stale data
27
+ return stripGraphStatus(filteredGraph);
26
28
  }
27
29
  /**
28
30
  * Open specific nodes by name
@@ -42,7 +44,8 @@ export async function openNodes(storage, threadId, names) {
42
44
  entities: filteredEntities,
43
45
  relations: filteredRelations,
44
46
  };
45
- return filteredGraph;
47
+ // Strip any persisted status values to prevent leaking stale data
48
+ return stripGraphStatus(filteredGraph);
46
49
  }
47
50
  /**
48
51
  * Query nodes with advanced filters
@@ -106,8 +109,10 @@ export async function queryNodes(storage, threadId, filters) {
106
109
  return false;
107
110
  return true;
108
111
  });
109
- return {
112
+ const filteredGraph = {
110
113
  entities: filteredEntities,
111
114
  relations: filteredRelations,
112
115
  };
116
+ // Strip any persisted status values to prevent leaking stale data
117
+ return stripGraphStatus(filteredGraph);
113
118
  }
@@ -2,6 +2,7 @@
2
2
  * Zod schema definitions for MCP tools
3
3
  */
4
4
  import { z } from "zod";
5
+ import { ARCHIVED_THRESHOLD } from "./queries/graph-reader.js";
5
6
  // Schema for Observation with versioning support
6
7
  export const ObservationSchema = z.object({
7
8
  id: z.string().describe("Unique observation ID"),
@@ -12,8 +13,11 @@ export const ObservationSchema = z.object({
12
13
  superseded_by: z.string().optional().describe("ID of observation that supersedes this one"),
13
14
  agentThreadId: z.string().describe("Thread that created this observation"),
14
15
  confidence: z.number().min(0).max(1).optional().describe("Confidence in accuracy (0-1, optional, inherits from entity if not set)"),
15
- importance: z.number().min(0).max(1).optional().describe("Importance for memory integrity (0-1, optional, inherits from entity if not set)")
16
+ importance: z.number().min(0).max(1).optional().describe("Importance for memory integrity (0-1, optional, inherits from entity if not set)"),
17
+ status: z.literal('ARCHIVED').optional().describe("Status indicator - set to 'ARCHIVED' for low-importance items")
16
18
  });
19
+ // Input schema for observations (excludes status field which is computed)
20
+ export const ObservationInputSchema = ObservationSchema.omit({ status: true }).strict();
17
21
  // Schema for existing tools
18
22
  export const EntitySchema = z.object({
19
23
  name: z.string().describe("Unique identifier for the entity"),
@@ -22,8 +26,13 @@ export const EntitySchema = z.object({
22
26
  agentThreadId: z.string().describe("Agent thread that created/modified this entity"),
23
27
  timestamp: z.string().describe("ISO 8601 timestamp of creation/modification"),
24
28
  confidence: z.number().min(0).max(1).describe("Confidence in the accuracy of this entity (0-1)"),
25
- importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
29
+ importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)"),
30
+ status: z.literal('ARCHIVED').optional().describe("Status indicator - set to 'ARCHIVED' for low-importance items")
26
31
  });
32
+ // Input schema for entities (excludes status field which is computed, uses input observations)
33
+ export const EntityInputSchema = EntitySchema.omit({ status: true, observations: true }).extend({
34
+ observations: z.array(ObservationInputSchema).describe("Versioned observations about this entity")
35
+ }).strict();
27
36
  export const RelationSchema = z.object({
28
37
  from: z.string().describe("Source entity name"),
29
38
  to: z.string().describe("Target entity name"),
@@ -31,8 +40,11 @@ export const RelationSchema = z.object({
31
40
  agentThreadId: z.string().describe("Agent thread that created/modified this relation"),
32
41
  timestamp: z.string().describe("ISO 8601 timestamp of creation/modification"),
33
42
  confidence: z.number().min(0).max(1).describe("Confidence in the accuracy of this relation (0-1)"),
34
- importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
43
+ importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)"),
44
+ status: z.literal('ARCHIVED').optional().describe("Status indicator - set to 'ARCHIVED' for low-importance items")
35
45
  });
46
+ // Input schema for relations (excludes status field which is computed)
47
+ export const RelationInputSchema = RelationSchema.omit({ status: true }).strict();
36
48
  // Schema for save_memory tool (Section 1 of spec)
37
49
  export const SaveMemoryRelationSchema = z.object({
38
50
  targetEntity: z.string().describe("Name of entity to connect to (must exist in this request)"),
@@ -147,7 +159,8 @@ export const UpdateObservationOutputSchema = z.object({
147
159
  });
148
160
  // Schema for read_graph tool
149
161
  export const ReadGraphInputSchema = z.object({
150
- threadId: z.string().min(1).describe("Thread ID for this conversation/project")
162
+ threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
163
+ minImportance: z.number().min(0).max(1).optional().default(ARCHIVED_THRESHOLD).describe(`Minimum importance threshold (0-1). Items with importance below this value are excluded. Items with importance between minImportance and ${ARCHIVED_THRESHOLD} are marked as ARCHIVED. Default: ${ARCHIVED_THRESHOLD}`)
151
164
  });
152
165
  // Schema for search_nodes tool
153
166
  export const SearchNodesInputSchema = z.object({
@@ -202,7 +215,7 @@ export const GetContextInputSchema = z.object({
202
215
  // Schema for create_entities tool
203
216
  export const CreateEntitiesInputSchema = z.object({
204
217
  threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
205
- entities: z.array(EntitySchema).describe("Array of entities to create")
218
+ entities: z.array(EntityInputSchema).describe("Array of entities to create")
206
219
  }).superRefine((data, ctx) => {
207
220
  const { threadId, entities } = data;
208
221
  entities.forEach((entity, index) => {
@@ -218,7 +231,7 @@ export const CreateEntitiesInputSchema = z.object({
218
231
  // Schema for create_relations tool
219
232
  export const CreateRelationsInputSchema = z.object({
220
233
  threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
221
- relations: z.array(RelationSchema).describe("Array of relations to create")
234
+ relations: z.array(RelationInputSchema).describe("Array of relations to create")
222
235
  }).superRefine((data, ctx) => {
223
236
  const { threadId, relations } = data;
224
237
  relations.forEach((relation, index) => {
@@ -269,7 +282,7 @@ export const DeleteObservationsInputSchema = z.object({
269
282
  // Schema for delete_relations tool
270
283
  export const DeleteRelationsInputSchema = z.object({
271
284
  threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
272
- relations: z.array(RelationSchema).describe("An array of relations to delete")
285
+ relations: z.array(RelationInputSchema).describe("An array of relations to delete")
273
286
  });
274
287
  // Schema for prune_memory tool
275
288
  export const PruneMemoryInputSchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-memory-enhanced",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Persistent memory for long conversations - MCP server for knowledge graph with atomic facts, cross-chat memory sharing, and multi-agent delegation",
5
5
  "license": "MIT",
6
6
  "mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",