cozo-memory 1.0.3 → 1.0.4

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,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MemoryServer = exports.USER_ENTITY_TYPE = exports.USER_ENTITY_NAME = exports.USER_ENTITY_ID = exports.DB_PATH = void 0;
8
8
  const embedding_service_1 = require("./embedding-service");
9
+ const export_import_service_1 = require("./export-import-service");
9
10
  const fastmcp_1 = require("fastmcp");
10
11
  const cozo_node_1 = require("cozo-node");
11
12
  const zod_1 = require("zod");
@@ -27,6 +28,47 @@ class MemoryServer {
27
28
  hybridSearch;
28
29
  inferenceEngine;
29
30
  initPromise;
31
+ // Metrics tracking
32
+ metrics = {
33
+ operations: {
34
+ create_entity: 0,
35
+ update_entity: 0,
36
+ delete_entity: 0,
37
+ add_observation: 0,
38
+ create_relation: 0,
39
+ search: 0,
40
+ graph_operations: 0
41
+ },
42
+ errors: {
43
+ total: 0,
44
+ by_operation: {}
45
+ },
46
+ performance: {
47
+ last_operation_ms: 0,
48
+ avg_operation_ms: 0,
49
+ total_operations: 0
50
+ }
51
+ };
52
+ trackOperation(operation, startTime) {
53
+ const duration = Date.now() - startTime;
54
+ this.metrics.performance.last_operation_ms = duration;
55
+ this.metrics.performance.total_operations++;
56
+ // Calculate running average
57
+ const prevAvg = this.metrics.performance.avg_operation_ms;
58
+ const n = this.metrics.performance.total_operations;
59
+ this.metrics.performance.avg_operation_ms = (prevAvg * (n - 1) + duration) / n;
60
+ // Track operation count
61
+ if (operation in this.metrics.operations) {
62
+ this.metrics.operations[operation]++;
63
+ }
64
+ }
65
+ trackError(operation) {
66
+ this.metrics.errors.total++;
67
+ if (!this.metrics.errors.by_operation[operation]) {
68
+ this.metrics.errors.by_operation[operation] = 0;
69
+ }
70
+ this.metrics.errors.by_operation[operation]++;
71
+ }
30
72
  constructor(dbPath = exports.DB_PATH) {
31
73
  const fullDbPath = DB_ENGINE === "sqlite" ? dbPath + ".db" : dbPath;
32
74
  this.db = new cozo_node_1.CozoDb(DB_ENGINE, fullDbPath);
@@ -934,6 +976,7 @@ class MemoryServer {
934
976
  });
935
977
  }
936
978
  async createEntity(args) {
979
+ const startTime = Date.now();
937
980
  try {
938
981
  if (!args.name || args.name.trim() === "") {
939
982
  return { error: "Entity name must not be empty" };
@@ -957,9 +1000,12 @@ class MemoryServer {
957
1000
  }
958
1001
  }
959
1002
  const id = (0, uuid_1.v4)();
960
- return this.createEntityWithId(id, args.name, args.type, args.metadata);
1003
+ const result = await this.createEntityWithId(id, args.name, args.type, args.metadata);
1004
+ this.trackOperation('create_entity', startTime);
1005
+ return result;
961
1006
  }
962
1007
  catch (error) {
1008
+ this.trackError('create_entity');
963
1009
  console.error("Error in create_entity:", error);
964
1010
  if (error.display) {
965
1011
  console.error("CozoDB Error Details:", error.display);
@@ -1168,6 +1214,28 @@ class MemoryServer {
1168
1214
  if (toEntity.rows.length === 0) {
1169
1215
  return { error: `Target entity with ID '${args.to_id}' not found` };
1170
1216
  }
1217
+ // FIX: Check for duplicate relations
1218
+ try {
1219
+ const existingRel = await this.db.run('?[from_id, to_id, relation_type, strength, metadata] := *relationship{from_id, to_id, relation_type, strength, metadata, @ "NOW"}, from_id = $from, to_id = $to, relation_type = $type', { from: args.from_id, to: args.to_id, type: args.relation_type });
1220
+ if (existingRel.rows.length > 0) {
1221
+ // Update existing relation instead of creating duplicate
1222
+ const now = Date.now() * 1000;
1223
+ await this.db.run(`
1224
+ ?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]]
1225
+ :put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
1226
+ `, {
1227
+ from_id: args.from_id,
1228
+ to_id: args.to_id,
1229
+ relation_type: args.relation_type,
1230
+ strength: args.strength ?? 1.0,
1231
+ metadata: args.metadata || {}
1232
+ });
1233
+ return { status: "Relationship updated (was duplicate)" };
1234
+ }
1235
+ }
1236
+ catch (e) {
1237
+ console.error('[Relation] Duplicate check failed, proceeding with insert:', e);
1238
+ }
1171
1239
  const now = Date.now() * 1000;
1172
1240
  await this.db.run(`?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]] :insert relationship {from_id, to_id, relation_type, created_at => strength, metadata}`, {
1173
1241
  from_id: args.from_id,
@@ -1707,23 +1775,54 @@ ids[id] <- $ids
1707
1775
  }
1708
1776
  }
1709
1777
  async deleteEntity(args) {
1778
+ const startTime = Date.now();
1710
1779
  try {
1780
+ console.error(`[Delete] Starting deletion for entity: ${args.entity_id}`);
1711
1781
  // 1. Check if entity exists
1712
1782
  const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.entity_id });
1713
1783
  if (entityRes.rows.length === 0) {
1784
+ console.error(`[Delete] Entity not found: ${args.entity_id}`);
1714
1785
  return { error: `Entity with ID '${args.entity_id}' not found` };
1715
1786
  }
1716
- // 2. Delete all related data in a transaction (block)
1787
+ console.error(`[Delete] Entity found: ${entityRes.rows[0][0]}`);
1788
+ // 2. Count related data before deletion
1789
+ const obsCount = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
1790
+ const relOutCount = await this.db.run('?[count(from_id)] := *relationship{from_id, to_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
1791
+ const relInCount = await this.db.run('?[count(to_id)] := *relationship{from_id, to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
1792
+ console.error(`[Delete] Related data: ${obsCount.rows[0][0]} observations, ${relOutCount.rows[0][0]} outgoing relations, ${relInCount.rows[0][0]} incoming relations`);
1793
+ // 3. Delete all related data in a transaction (block)
1794
+ console.error(`[Delete] Executing deletion transaction...`);
1717
1795
  await this.db.run(`
1718
1796
  { ?[id, created_at] := *observation{id, entity_id, created_at}, entity_id = $target_id :rm observation {id, created_at} }
1719
1797
  { ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, from_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
1720
1798
  { ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, to_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
1721
1799
  { ?[id, created_at] := *entity{id, created_at}, id = $target_id :rm entity {id, created_at} }
1722
1800
  `, { target_id: args.entity_id });
1723
- return { status: "Entity and all related data deleted" };
1801
+ // 4. Verify deletion
1802
+ const verifyEntity = await this.db.run('?[id] := *entity{id, @ "NOW"}, id = $id', { id: args.entity_id });
1803
+ const verifyObs = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
1804
+ const verifyRelOut = await this.db.run('?[count(from_id)] := *relationship{from_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
1805
+ const verifyRelIn = await this.db.run('?[count(to_id)] := *relationship{to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
1806
+ console.error(`[Delete] Verification: entity exists=${verifyEntity.rows.length > 0}, observations=${verifyObs.rows[0][0]}, outgoing_relations=${verifyRelOut.rows[0][0]}, incoming_relations=${verifyRelIn.rows[0][0]}`);
1807
+ if (verifyEntity.rows.length > 0) {
1808
+ console.error(`[Delete] WARNING: Entity still visible after deletion!`);
1809
+ }
1810
+ else {
1811
+ console.error(`[Delete] ✓ Entity successfully deleted`);
1812
+ }
1813
+ this.trackOperation('delete_entity', startTime);
1814
+ return {
1815
+ status: "Entity and all related data deleted",
1816
+ deleted: {
1817
+ observations: obsCount.rows[0][0],
1818
+ outgoing_relations: relOutCount.rows[0][0],
1819
+ incoming_relations: relInCount.rows[0][0]
1820
+ }
1821
+ };
1724
1822
  }
1725
1823
  catch (error) {
1726
- console.error("Error during deletion:", error);
1824
+ console.error("[Delete] Error during deletion:", error);
1825
+ this.trackError('delete_entity');
1727
1826
  return { error: "Deletion failed", message: error.message };
1728
1827
  }
1729
1828
  }
@@ -1865,6 +1964,26 @@ ids[id] <- $ids
1865
1964
  to_id = existingId;
1866
1965
  }
1867
1966
  }
1967
+ // FIX: Validate that both entities exist before creating relation
1968
+ try {
1969
+ const [fromExists, toExists] = await Promise.all([
1970
+ this.db.run('?[id, name] := *entity{id, name, @ "NOW"}, id = $id', { id: from_id }),
1971
+ this.db.run('?[id, name] := *entity{id, name, @ "NOW"}, id = $id', { id: to_id })
1972
+ ]);
1973
+ if (fromExists.rows.length === 0) {
1974
+ return { error: `Transaction failed: Source entity '${from_id}' not found in operation ${i}` };
1975
+ }
1976
+ if (toExists.rows.length === 0) {
1977
+ return { error: `Transaction failed: Target entity '${to_id}' not found in operation ${i}` };
1978
+ }
1979
+ }
1980
+ catch (e) {
1981
+ return { error: `Transaction failed: Entity validation error in operation ${i}: ${e.message}` };
1982
+ }
1983
+ // FIX: Check for self-reference
1984
+ if (from_id === to_id) {
1985
+ return { error: `Transaction failed: Self-references not allowed in operation ${i}` };
1986
+ }
1868
1987
  const now = Date.now() * 1000;
1869
1988
  allParams[`rel_from${suffix}`] = from_id;
1870
1989
  allParams[`rel_to${suffix}`] = to_id;
@@ -1991,6 +2110,22 @@ ids[id] <- $ids
1991
2110
  await this.initPromise;
1992
2111
  return this.hybridSearch.graphRag(args);
1993
2112
  }
2113
+ async exportMemory(args) {
2114
+ await this.initPromise;
2115
+ const dbService = { run: (query, params) => this.db.run(query, params) };
2116
+ const exportService = new export_import_service_1.ExportImportService(dbService);
2117
+ return exportService.exportMemory(args);
2118
+ }
2119
+ async importMemory(args) {
2120
+ await this.initPromise;
2121
+ const dbService = { run: (query, params) => this.db.run(query, params) };
2122
+ const exportService = new export_import_service_1.ExportImportService(dbService);
2123
+ return exportService.importMemory(args.data, {
2124
+ sourceFormat: args.sourceFormat,
2125
+ mergeStrategy: args.mergeStrategy,
2126
+ defaultEntityType: args.defaultEntityType
2127
+ });
2128
+ }
1994
2129
  registerTools() {
1995
2130
  const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
1996
2131
  const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
@@ -2881,6 +3016,23 @@ Supported actions:
2881
3016
  });
2882
3017
  const ManageSystemSchema = zod_1.z.discriminatedUnion("action", [
2883
3018
  zod_1.z.object({ action: zod_1.z.literal("health") }),
3019
+ zod_1.z.object({ action: zod_1.z.literal("metrics") }),
3020
+ zod_1.z.object({
3021
+ action: zod_1.z.literal("export_memory"),
3022
+ format: zod_1.z.enum(["json", "markdown", "obsidian"]).describe("Export format"),
3023
+ includeMetadata: zod_1.z.boolean().optional().describe("Include metadata in export"),
3024
+ includeRelationships: zod_1.z.boolean().optional().describe("Include relationships in export"),
3025
+ includeObservations: zod_1.z.boolean().optional().describe("Include observations in export"),
3026
+ entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types"),
3027
+ since: zod_1.z.number().optional().describe("Unix timestamp (ms) - only export data created after this time"),
3028
+ }),
3029
+ zod_1.z.object({
3030
+ action: zod_1.z.literal("import_memory"),
3031
+ data: zod_1.z.union([zod_1.z.string(), zod_1.z.any()]).describe("Import data (JSON string or object)"),
3032
+ sourceFormat: zod_1.z.enum(["mem0", "memgpt", "markdown", "cozo"]).describe("Source format"),
3033
+ mergeStrategy: zod_1.z.enum(["skip", "overwrite", "merge"]).optional().default("skip").describe("How to handle existing entities"),
3034
+ defaultEntityType: zod_1.z.string().optional().default("Note").describe("Default type for imported entities"),
3035
+ }),
2884
3036
  zod_1.z.object({
2885
3037
  action: zod_1.z.literal("snapshot_create"),
2886
3038
  metadata: MetadataSchema.optional().describe("Additional metadata for the snapshot"),
@@ -2911,8 +3063,18 @@ Supported actions:
2911
3063
  ]);
2912
3064
  const ManageSystemParameters = zod_1.z.object({
2913
3065
  action: zod_1.z
2914
- .enum(["health", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
3066
+ .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
2915
3067
  .describe("Action (determines which fields are required)"),
3068
+ format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
3069
+ includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
3070
+ includeRelationships: zod_1.z.boolean().optional().describe("Include relationships (for export_memory)"),
3071
+ includeObservations: zod_1.z.boolean().optional().describe("Include observations (for export_memory)"),
3072
+ entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types (for export_memory)"),
3073
+ since: zod_1.z.number().optional().describe("Unix timestamp in ms (for export_memory)"),
3074
+ data: zod_1.z.union([zod_1.z.string(), zod_1.z.any()]).optional().describe("Import data (for import_memory)"),
3075
+ sourceFormat: zod_1.z.enum(["mem0", "memgpt", "markdown", "cozo"]).optional().describe("Source format (for import_memory)"),
3076
+ mergeStrategy: zod_1.z.enum(["skip", "overwrite", "merge"]).optional().describe("Merge strategy (for import_memory)"),
3077
+ defaultEntityType: zod_1.z.string().optional().describe("Default entity type (for import_memory)"),
2916
3078
  snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
2917
3079
  snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
2918
3080
  metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
@@ -2927,7 +3089,17 @@ Supported actions:
2927
3089
  name: "manage_system",
2928
3090
  description: `System maintenance and memory management. Select operation via 'action'.
2929
3091
  Supported actions:
2930
- - 'health': Status check. Returns DB counts and embedding cache statistics.
3092
+ - 'health': Status check. Returns DB counts, embedding cache statistics, and performance metrics.
3093
+ - 'metrics': Detailed metrics. Returns operation counts, error statistics, and performance data.
3094
+ - 'export_memory': Export memory to various formats. Params: { format: 'json'|'markdown'|'obsidian', includeMetadata?: boolean, includeRelationships?: boolean, includeObservations?: boolean, entityTypes?: string[], since?: number }.
3095
+ * format='json': Native Cozo format (re-importable)
3096
+ * format='markdown': Human-readable markdown document
3097
+ * format='obsidian': ZIP archive with wiki-links, ready for Obsidian vault
3098
+ - 'import_memory': Import memory from external sources. Params: { data: string|object, sourceFormat: 'mem0'|'memgpt'|'markdown'|'cozo', mergeStrategy?: 'skip'|'overwrite'|'merge', defaultEntityType?: string }.
3099
+ * sourceFormat='mem0': Import from Mem0 format (user_id becomes entity)
3100
+ * sourceFormat='memgpt': Import from MemGPT archival/recall memory
3101
+ * sourceFormat='markdown': Parse markdown sections as entities
3102
+ * sourceFormat='cozo': Import from native Cozo JSON export
2931
3103
  - 'snapshot_create': Creates a backup point. Params: { metadata?: object }.
2932
3104
  - 'snapshot_list': Lists all available snapshots.
2933
3105
  - 'snapshot_diff': Compares two snapshots. Params: { snapshot_id_a: string, snapshot_id_b: string }.
@@ -2957,7 +3129,16 @@ Supported actions:
2957
3129
  observations: obsResult.rows.length,
2958
3130
  relations: relResult.rows.length,
2959
3131
  },
2960
- performance: { embedding_cache: this.embeddingService.getCacheStats() },
3132
+ performance: {
3133
+ embedding_cache: this.embeddingService.getCacheStats(),
3134
+ last_operation_ms: this.metrics.performance.last_operation_ms,
3135
+ avg_operation_ms: this.metrics.performance.avg_operation_ms,
3136
+ total_operations: this.metrics.performance.total_operations
3137
+ },
3138
+ metrics: {
3139
+ operations: this.metrics.operations,
3140
+ errors: this.metrics.errors
3141
+ },
2961
3142
  timestamp: new Date().toISOString(),
2962
3143
  });
2963
3144
  }
@@ -2965,6 +3146,60 @@ Supported actions:
2965
3146
  return JSON.stringify({ status: "error", error: error.message, timestamp: new Date().toISOString() });
2966
3147
  }
2967
3148
  }
3149
+ if (input.action === "metrics") {
3150
+ try {
3151
+ return JSON.stringify({
3152
+ operations: this.metrics.operations,
3153
+ errors: this.metrics.errors,
3154
+ performance: this.metrics.performance,
3155
+ embedding_cache: this.embeddingService.getCacheStats(),
3156
+ timestamp: new Date().toISOString()
3157
+ });
3158
+ }
3159
+ catch (error) {
3160
+ return JSON.stringify({ error: "Failed to retrieve metrics", message: error.message });
3161
+ }
3162
+ }
3163
+ if (input.action === "export_memory") {
3164
+ try {
3165
+ const result = await this.exportMemory({
3166
+ format: input.format,
3167
+ includeMetadata: input.includeMetadata,
3168
+ includeRelationships: input.includeRelationships,
3169
+ includeObservations: input.includeObservations,
3170
+ entityTypes: input.entityTypes,
3171
+ since: input.since
3172
+ });
3173
+ if (result.format === 'obsidian' && result.zipBuffer) {
3174
+ // For Obsidian ZIP, return base64 encoded buffer
3175
+ return JSON.stringify({
3176
+ format: 'obsidian',
3177
+ data: result.zipBuffer.toString('base64'),
3178
+ stats: result.stats,
3179
+ encoding: 'base64',
3180
+ filename: `memory_export_${Date.now()}.zip`
3181
+ });
3182
+ }
3183
+ return JSON.stringify(result);
3184
+ }
3185
+ catch (error) {
3186
+ return JSON.stringify({ error: "Export failed", message: error.message });
3187
+ }
3188
+ }
3189
+ if (input.action === "import_memory") {
3190
+ try {
3191
+ const result = await this.importMemory({
3192
+ data: input.data,
3193
+ sourceFormat: input.sourceFormat,
3194
+ mergeStrategy: input.mergeStrategy,
3195
+ defaultEntityType: input.defaultEntityType
3196
+ });
3197
+ return JSON.stringify(result);
3198
+ }
3199
+ catch (error) {
3200
+ return JSON.stringify({ error: "Import failed", message: error.message });
3201
+ }
3202
+ }
2968
3203
  if (input.action === "snapshot_create") {
2969
3204
  try {
2970
3205
  // Optimization: Sequential execution and count aggregation instead of full fetch
@@ -324,9 +324,16 @@ class InferenceEngine {
324
324
  for (const r of res.rows) {
325
325
  if (!r || r.length < 5)
326
326
  continue;
327
+ const fromId = String(r[0]);
328
+ const toId = String(r[1]);
329
+ // FIX: Filter out self-references
330
+ if (fromId === toId) {
331
+ console.error(`[Inference] Skipping self-reference in custom rule ${ruleName}: ${fromId} -> ${toId}`);
332
+ continue;
333
+ }
327
334
  results.push({
328
- from_id: String(r[0]),
329
- to_id: String(r[1]),
335
+ from_id: fromId,
336
+ to_id: toId,
330
337
  relation_type: String(r[2]),
331
338
  confidence: Number(r[3]),
332
339
  reason: String(r[4]),