@memstack/core 0.5.0 → 0.7.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/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  > The open-source memory layer for AI agents — store, retrieve, summarize, and prune.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@memstack/core)](https://www.npmjs.com/package/@memstack/core)
6
+ [![CI](https://github.com/isiomaC/memstack/actions/workflows/ci.yml/badge.svg)](https://github.com/isiomaC/memstack/actions/workflows/ci.yml)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![MCP Reference](https://img.shields.io/badge/MCP-LLM%20Reference-blue)](https://gitmcp.io/isiomaC/memstack)
7
9
 
8
10
  ```bash
9
11
  npm install @memstack/core
@@ -671,7 +673,7 @@ new OpenAIEmbeddingAdapter({ apiKey: "...", baseURL: "https://api.voyageai.com/v
671
673
 
672
674
  ### Storage Adapters
673
675
 
674
- MemStack ships with **11 production-ready storage adapters (7 experimental)** — every major backend, zero peer dependencies, all client-injected.
676
+ MemStack ships with **12 production-ready storage adapters (6 experimental)** — every major backend, zero peer dependencies, all client-injected.
675
677
 
676
678
  ### Production (e2e verified against real instances)
677
679
 
@@ -687,6 +689,7 @@ MemStack ships with **11 production-ready storage adapters (7 experimental)**
687
689
  | Adapter | Backend | Vector search |
688
690
  |---|---|---|
689
691
  | `PostgresStorageAdapter` | PostgreSQL + pgvector | HNSW native |
692
+ | `SQLiteStorageAdapter` | SQLite (better-sqlite3) | Cosine in-memory |
690
693
 
691
694
  **Vector databases:**
692
695
  | Adapter | Backend |
@@ -712,7 +715,6 @@ Available via direct source import. Not yet in the barrel export — uncomment i
712
715
 
713
716
  | Adapter | Backend | Blocker |
714
717
  |---|---|---|
715
- | `SQLiteStorageAdapter` | SQLite (better-sqlite3) | Native binary for Node 24 |
716
718
  | `TursoStorageAdapter` | Turso (libsql) | Cloud-only (needs Turso account) |
717
719
  | `ChromaStorageAdapter` | ChromaDB | Embedding function dependency |
718
720
  | `PineconeStorageAdapter` | Pinecone | Cloud-only (needs API key) |
@@ -781,7 +783,6 @@ class MyStorage implements StorageProvider {
781
783
  | Neo4j | Neo4j vector index | No | ✅ Production |
782
784
  | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
783
785
  | SQLite | Cosine in-memory | Yes | ✅ Production |
784
- | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
785
786
 
786
787
  ---
787
788
 
@@ -797,18 +798,28 @@ const ms = new MemStack({
797
798
  embedding?: EmbeddingProvider, // Optional — for semantic search
798
799
  storage?: StorageProvider, // Optional — defaults to InMemoryStorageAdapter
799
800
  defaults?: {
800
- summarizationThreshold?: number, // Auto-summarize every N interactions. Default: 100
801
+ summarizationThreshold?: number, // Auto-summarize every N process() calls. Default: 100
801
802
  embedOnStore?: boolean, // Auto-embed on store(). Default: true
802
- pruneStrategy?: PruneStrategy, // Auto-prune on every store(). Default: disabled
803
+ pruneStrategy?: PruneStrategy, // Auto-prune during process() (throttled). Default: disabled
804
+ pruneInterval?: number, // Run auto-prune every N process() calls. Default: 100
805
+ autoImportance?: boolean, // LLM-score importance in process() when not provided. Default: false
806
+ autoTags?: boolean, // LLM-extract tags in process() when not provided. Default: false
807
+ summarizationPrompt?: string, // Custom prompt for the summarizer
803
808
  },
804
809
  hooks?: {
805
810
  onMemoryStored?: (memory: Memory) => void;
806
811
  onMemoryPruned?: (ids: string[]) => void;
807
812
  onSummaryCreated?: (summary: Memory, deletedCount: number) => void;
813
+ onError?: (error: Error, context: string) => void;
808
814
  },
809
815
  });
810
816
  ```
811
817
 
818
+ > **Auto-behaviors run inside `process()`, not `store()`.** `process()` tracks a
819
+ > per-actor call count: summarization fires every `summarizationThreshold` calls,
820
+ > and pruning fires every `pruneInterval` calls (when `pruneStrategy` is set).
821
+ > `store()` is the low-level write and never triggers these.
822
+
812
823
  ### Memory Subsystem
813
824
 
814
825
  All methods accessible via `ms.memory.*`:
@@ -857,6 +868,8 @@ const data = JSON.parse(fs.readFileSync("state.json", "utf-8"));
857
868
  await ms2.import(data);
858
869
  ```
859
870
 
871
+ Each memory's original `createdAt` is preserved on import, so `export` → `import` is a lossless round-trip — safe for backups and cross-backend migration (e.g. disk → Postgres). All storage adapters honor a `createdAt` supplied on `store()`/`storeBatch()`; when omitted, they default to the current time.
872
+
860
873
  ### Health & Close
861
874
 
862
875
  ```typescript
@@ -874,14 +887,17 @@ await ms.close(); // graceful shutdown
874
887
  const ms = new MemStack({
875
888
  llm: new OpenAILLMAdapter({ apiKey: "..." }),
876
889
 
877
- // Defaults control auto-behavior
890
+ // Defaults control auto-behavior (all applied during process())
878
891
  defaults: {
879
- summarizationThreshold: 50, // Summarize every 50 interactions (default: 100)
892
+ summarizationThreshold: 50, // Summarize every 50 process() calls (default: 100)
880
893
  embedOnStore: false, // Don't auto-embed — saves API costs
881
- pruneStrategy: { // Auto-clean on every store()
894
+ pruneStrategy: { // Auto-clean during process(), throttled by pruneInterval
882
895
  type: "byAge",
883
896
  maxAge: 90 * 86400000, // 90 days
884
897
  },
898
+ pruneInterval: 100, // Run the prune check every 100 process() calls (default: 100)
899
+ autoImportance: true, // Let the LLM score importance when you don't pass one
900
+ autoTags: true, // Let the LLM extract tags when you don't pass any
885
901
  },
886
902
 
887
903
  // Hooks for observability
@@ -889,6 +905,7 @@ const ms = new MemStack({
889
905
  onMemoryStored: (m) => logger.debug("memory:stored", { id: m.id, actor: m.actorId }),
890
906
  onMemoryPruned: (ids) => logger.info("memory:pruned", { count: ids.length }),
891
907
  onSummaryCreated: (summary, n) => logger.info("memory:summarized", { count: n }),
908
+ onError: (err, context) => logger.error("memory:error", { context, message: err.message }),
892
909
  },
893
910
  });
894
911
  ```
package/dist/index.cjs CHANGED
@@ -600,7 +600,7 @@ var MemoryStore = class {
600
600
  }
601
601
  async prune(strategy) {
602
602
  await this._ensureInit();
603
- const allMemories = await this.storage.retrieve({ limit: this.limits.pruneScan });
603
+ const allMemories = await this.storage.retrieve({ actorId: strategy.actorId, limit: this.limits.pruneScan });
604
604
  const kept = this.pruner.execute(allMemories, strategy);
605
605
  const keptIds = new Set(kept.map((k) => k.id));
606
606
  const prunedIds = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
@@ -612,7 +612,7 @@ var MemoryStore = class {
612
612
  }
613
613
  async dryRunPrune(strategy) {
614
614
  await this._ensureInit();
615
- const allMemories = await this.storage.retrieve({ limit: this.limits.pruneScan });
615
+ const allMemories = await this.storage.retrieve({ actorId: strategy.actorId, limit: this.limits.pruneScan });
616
616
  const kept = this.pruner.execute(allMemories, strategy);
617
617
  const keptIds = new Set(kept.map((k) => k.id));
618
618
  const wouldPrune = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
@@ -746,7 +746,7 @@ var InMemoryStorageAdapter = class {
746
746
  sourceId: input.sourceId,
747
747
  metadata: input.metadata ?? {},
748
748
  expiresAt: input.expiresAt,
749
- createdAt: now,
749
+ createdAt: input.createdAt ?? now,
750
750
  _touchedAt: now
751
751
  };
752
752
  this.memories.set(memory.id, memory);
@@ -796,8 +796,11 @@ var InMemoryStorageAdapter = class {
796
796
  results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
797
797
  }
798
798
  if (query.query) {
799
- const q = query.query.toLowerCase();
800
- results = results.filter((m) => m.content.toLowerCase().includes(q));
799
+ const terms = query.query.toLowerCase().split(/\s+/).filter(Boolean);
800
+ results = results.filter((m) => {
801
+ const content = m.content.toLowerCase();
802
+ return terms.some((t) => content.includes(t));
803
+ });
801
804
  }
802
805
  if (query.createdAfter) {
803
806
  results = results.filter((m) => m.createdAt >= query.createdAfter);
@@ -983,7 +986,19 @@ var MemStack = class {
983
986
  if (snapshot.version !== 1) {
984
987
  throw validationError(`Unsupported snapshot version: ${snapshot.version}`);
985
988
  }
986
- await this.memory.storeBatch(snapshot.memories);
989
+ const memories = snapshot.memories.map((m, i) => {
990
+ const createdAt = typeof m.createdAt === "string" ? new Date(m.createdAt) : m.createdAt;
991
+ const expiresAt = typeof m.expiresAt === "string" ? new Date(m.expiresAt) : m.expiresAt;
992
+ const label = m.id ? `id: ${m.id}` : `index ${i}`;
993
+ if (createdAt && isNaN(createdAt.getTime())) {
994
+ throw validationError(`Invalid createdAt for memory (${label}): ${String(m.createdAt)}`);
995
+ }
996
+ if (expiresAt && isNaN(expiresAt.getTime())) {
997
+ throw validationError(`Invalid expiresAt for memory (${label}): ${String(m.expiresAt)}`);
998
+ }
999
+ return { ...m, createdAt, expiresAt };
1000
+ });
1001
+ await this.memory.storeBatch(memories);
987
1002
  }
988
1003
  async health() {
989
1004
  const status = { storage: false, llm: false, embedding: false };
@@ -1070,7 +1085,7 @@ var DiskStorageAdapter = class {
1070
1085
  sourceId: input.sourceId,
1071
1086
  metadata: input.metadata ?? {},
1072
1087
  expiresAt: input.expiresAt,
1073
- createdAt: now,
1088
+ createdAt: input.createdAt ?? now,
1074
1089
  _touchedAt: now.toISOString()
1075
1090
  };
1076
1091
  await this._withWriteLock(input.actorId, async () => {
@@ -1103,7 +1118,7 @@ var DiskStorageAdapter = class {
1103
1118
  sourceId: input.sourceId,
1104
1119
  metadata: input.metadata ?? {},
1105
1120
  expiresAt: input.expiresAt,
1106
- createdAt: now,
1121
+ createdAt: input.createdAt ?? now,
1107
1122
  _touchedAt: now.toISOString()
1108
1123
  };
1109
1124
  all.push(memory);
@@ -1210,8 +1225,11 @@ var DiskStorageAdapter = class {
1210
1225
  records = records.filter((r) => r.tags?.some((t) => query.tags.includes(t)));
1211
1226
  }
1212
1227
  if (query.query) {
1213
- const q = query.query.toLowerCase();
1214
- records = records.filter((r) => r.content.toLowerCase().includes(q));
1228
+ const terms = query.query.toLowerCase().split(/\s+/).filter(Boolean);
1229
+ records = records.filter((r) => {
1230
+ const content = r.content.toLowerCase();
1231
+ return terms.some((t) => content.includes(t));
1232
+ });
1215
1233
  }
1216
1234
  results.push(...records);
1217
1235
  }
@@ -1302,9 +1320,14 @@ var DiskStorageAdapter = class {
1302
1320
  }
1303
1321
  }
1304
1322
  async _writeFile(actorId, records) {
1323
+ const filePath = this._filePath(actorId);
1324
+ const tmpPath = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
1305
1325
  try {
1306
- await promises.writeFile(this._filePath(actorId), JSON.stringify(records, null, 2), "utf-8");
1326
+ await promises.writeFile(tmpPath, JSON.stringify(records, null, 2), "utf-8");
1327
+ await promises.rename(tmpPath, filePath);
1307
1328
  } catch (err) {
1329
+ await promises.unlink(tmpPath).catch(() => {
1330
+ });
1308
1331
  throw storageError(`Failed to write memories for actor ${actorId}: ${err.message}`);
1309
1332
  }
1310
1333
  }
@@ -1316,15 +1339,57 @@ var DiskStorageAdapter = class {
1316
1339
  });
1317
1340
  this._writeLocks.set(actorId, next);
1318
1341
  await prev;
1342
+ const releaseFileLock = await this._acquireFileLock(actorId);
1319
1343
  try {
1320
1344
  await fn();
1321
1345
  } finally {
1346
+ await releaseFileLock();
1322
1347
  resolve();
1323
1348
  if (this._writeLocks.get(actorId) === next) {
1324
1349
  this._writeLocks.delete(actorId);
1325
1350
  }
1326
1351
  }
1327
1352
  }
1353
+ /**
1354
+ * Acquires an exclusive cross-process lock for `actorId` via `open(path, "wx")`
1355
+ * (atomic create-if-absent). Retries with jittered backoff while the lock is
1356
+ * held elsewhere, and steals locks left behind by a crashed process once
1357
+ * they're older than `staleMs`.
1358
+ */
1359
+ async _acquireFileLock(actorId) {
1360
+ const lockPath = `${this._filePath(actorId)}.lock`;
1361
+ const staleMs = 1e4;
1362
+ const timeoutMs = 3e4;
1363
+ const start = Date.now();
1364
+ while (true) {
1365
+ try {
1366
+ const handle = await promises.open(lockPath, "wx");
1367
+ await handle.close();
1368
+ return async () => {
1369
+ await promises.unlink(lockPath).catch(() => {
1370
+ });
1371
+ };
1372
+ } catch (err) {
1373
+ if (err.code !== "EEXIST") {
1374
+ throw storageError(`Failed to acquire lock for actor ${actorId}: ${err.message}`);
1375
+ }
1376
+ try {
1377
+ const st = await promises.stat(lockPath);
1378
+ if (Date.now() - st.mtimeMs > staleMs) {
1379
+ await promises.unlink(lockPath).catch(() => {
1380
+ });
1381
+ continue;
1382
+ }
1383
+ } catch {
1384
+ continue;
1385
+ }
1386
+ if (Date.now() - start > timeoutMs) {
1387
+ throw storageError(`Timed out waiting for disk storage lock: ${lockPath}`);
1388
+ }
1389
+ await new Promise((r) => setTimeout(r, 15 + Math.random() * 35));
1390
+ }
1391
+ }
1392
+ }
1328
1393
  async _listActors() {
1329
1394
  try {
1330
1395
  const entries = await promises.readdir(this.dir);
@@ -1382,7 +1447,7 @@ var MarkdownStorageAdapter = class {
1382
1447
  sourceId: input.sourceId,
1383
1448
  metadata: input.metadata ?? {},
1384
1449
  expiresAt: input.expiresAt,
1385
- createdAt: now
1450
+ createdAt: input.createdAt ?? now
1386
1451
  };
1387
1452
  const filePath = this._filePath(input.actorId);
1388
1453
  const block = this._formatBlock(memory);
@@ -1459,8 +1524,11 @@ var MarkdownStorageAdapter = class {
1459
1524
  results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
1460
1525
  }
1461
1526
  if (query.query) {
1462
- const q = query.query.toLowerCase();
1463
- results = results.filter((m) => m.content.toLowerCase().includes(q));
1527
+ const terms = query.query.toLowerCase().split(/\s+/).filter(Boolean);
1528
+ results = results.filter((m) => {
1529
+ const content = m.content.toLowerCase();
1530
+ return terms.some((t) => content.includes(t));
1531
+ });
1464
1532
  }
1465
1533
  switch (query.strategy) {
1466
1534
  case "recent":
@@ -2011,7 +2079,7 @@ var PostgresStorageAdapter = class {
2011
2079
  sourceId: input.sourceId,
2012
2080
  metadata: input.metadata ?? {},
2013
2081
  expiresAt: input.expiresAt,
2014
- createdAt: new Date(now)
2082
+ createdAt: input.createdAt ?? new Date(now)
2015
2083
  };
2016
2084
  }
2017
2085
  async storeBatch(inputs) {
@@ -2214,7 +2282,7 @@ var RedisStorageAdapter = class {
2214
2282
  sourceId: input.sourceId,
2215
2283
  metadata: input.metadata ?? {},
2216
2284
  expiresAt: input.expiresAt,
2217
- createdAt: now,
2285
+ createdAt: input.createdAt ?? now,
2218
2286
  _touchedAt: now.toISOString()
2219
2287
  };
2220
2288
  await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
@@ -2238,7 +2306,7 @@ var RedisStorageAdapter = class {
2238
2306
  sourceId: input.sourceId,
2239
2307
  metadata: input.metadata ?? {},
2240
2308
  expiresAt: input.expiresAt,
2241
- createdAt: now,
2309
+ createdAt: input.createdAt ?? now,
2242
2310
  _touchedAt: now.toISOString()
2243
2311
  };
2244
2312
  await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
@@ -2434,7 +2502,7 @@ var QdrantStorageAdapter = class {
2434
2502
  sourceId: input.sourceId,
2435
2503
  metadata: input.metadata ?? {},
2436
2504
  expiresAt: input.expiresAt,
2437
- createdAt: now
2505
+ createdAt: input.createdAt ?? now
2438
2506
  };
2439
2507
  const qdrantId = crypto.randomUUID();
2440
2508
  await this.client.upsert(this.collectionName, {
@@ -2466,7 +2534,7 @@ var QdrantStorageAdapter = class {
2466
2534
  sourceId: input.sourceId,
2467
2535
  metadata: input.metadata ?? {},
2468
2536
  expiresAt: input.expiresAt,
2469
- createdAt: now
2537
+ createdAt: input.createdAt ?? now
2470
2538
  };
2471
2539
  results.push(memory);
2472
2540
  const qid = crypto.randomUUID();
@@ -2644,10 +2712,13 @@ var Neo4jStorageAdapter = class {
2644
2712
  sourceId: input.sourceId ?? null,
2645
2713
  metadata: JSON.stringify(input.metadata ?? {}),
2646
2714
  expiresAt: input.expiresAt?.toISOString() ?? null,
2647
- createdAt: now,
2715
+ createdAt: input.createdAt?.toISOString() ?? now,
2648
2716
  embedding: input.embedding ?? null
2649
2717
  };
2650
- const { createdAt: _, ...updateProps } = props;
2718
+ const updateProps = { ...props };
2719
+ if (!input.createdAt) {
2720
+ delete updateProps.createdAt;
2721
+ }
2651
2722
  const session = this.driver.session({ database: this.database });
2652
2723
  try {
2653
2724
  await session.run(
@@ -2669,7 +2740,7 @@ var Neo4jStorageAdapter = class {
2669
2740
  sourceId: input.sourceId,
2670
2741
  metadata: input.metadata ?? {},
2671
2742
  expiresAt: input.expiresAt,
2672
- createdAt: new Date(now)
2743
+ createdAt: input.createdAt ?? new Date(now)
2673
2744
  };
2674
2745
  }
2675
2746
  async storeBatch(inputs) {
@@ -2906,7 +2977,7 @@ var WeaviateStorageAdapter = class {
2906
2977
  sourceId: input.sourceId,
2907
2978
  metadata: input.metadata ?? {},
2908
2979
  expiresAt: input.expiresAt,
2909
- createdAt: now
2980
+ createdAt: input.createdAt ?? now
2910
2981
  };
2911
2982
  const wvId = await this.collection().data.insert({
2912
2983
  id: crypto.randomUUID(),
@@ -3139,7 +3210,7 @@ var LanceDBStorageAdapter = class {
3139
3210
  sourceId: input.sourceId,
3140
3211
  metadata: input.metadata ?? {},
3141
3212
  expiresAt: input.expiresAt,
3142
- createdAt: now
3213
+ createdAt: input.createdAt ?? now
3143
3214
  };
3144
3215
  }
3145
3216
  async storeBatch(inputs) {
@@ -3307,7 +3378,7 @@ var MongoDBStorageAdapter = class {
3307
3378
  sourceId: input.sourceId,
3308
3379
  metadata: input.metadata ?? {},
3309
3380
  expiresAt: input.expiresAt,
3310
- createdAt: now
3381
+ createdAt: input.createdAt ?? now
3311
3382
  };
3312
3383
  await this.collection.insertOne(this.toDoc(memory));
3313
3384
  return memory;
@@ -3536,7 +3607,7 @@ var SQLiteStorageAdapter = class {
3536
3607
  sourceId: input.sourceId,
3537
3608
  metadata: input.metadata ?? {},
3538
3609
  expiresAt: input.expiresAt,
3539
- createdAt: new Date(now)
3610
+ createdAt: input.createdAt ?? new Date(now)
3540
3611
  };
3541
3612
  }
3542
3613
  async storeBatch(inputs) {
@@ -3809,11 +3880,12 @@ var OpenAILLMAdapter = class {
3809
3880
  try {
3810
3881
  const parsed = JSON.parse(data);
3811
3882
  const content = parsed.choices?.[0]?.delta?.content ?? "";
3812
- if (parsed.usage) {
3813
- promptTokens = parsed.usage.prompt_tokens;
3814
- completionTokens = parsed.usage.completion_tokens;
3883
+ const usage = parsed.usage;
3884
+ if (usage) {
3885
+ promptTokens = usage.prompt_tokens;
3886
+ completionTokens = usage.completion_tokens;
3815
3887
  }
3816
- if (content) {
3888
+ if (content || usage) {
3817
3889
  yield {
3818
3890
  text: content,
3819
3891
  tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens }
@@ -3869,7 +3941,15 @@ var AnthropicLLMAdapter = class {
3869
3941
  };
3870
3942
  } catch (err) {
3871
3943
  if (err instanceof MemStackError) throw err;
3872
- throw new MemStackError("LLM_ERROR", `Anthropic request failed: ${err instanceof Error ? err.message : String(err)}`, {
3944
+ const msg = err instanceof Error ? err.message : String(err);
3945
+ const code = err?.code;
3946
+ const isMissingSdk = msg.includes("@anthropic-ai/sdk") && (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || /cannot find (package|module)/i.test(msg) || /failed to (load|resolve)/i.test(msg));
3947
+ if (isMissingSdk) {
3948
+ throw new MemStackError("LLM_ERROR", "Anthropic adapter requires @anthropic-ai/sdk. Install it: npm install @anthropic-ai/sdk", {
3949
+ retryable: false
3950
+ });
3951
+ }
3952
+ throw new MemStackError("LLM_ERROR", `Anthropic request failed: ${msg}`, {
3873
3953
  retryable: true
3874
3954
  });
3875
3955
  }
@@ -4158,7 +4238,7 @@ var GroqLLMAdapter = class {
4158
4238
  promptTokens = usage.prompt_tokens;
4159
4239
  completionTokens = usage.completion_tokens;
4160
4240
  }
4161
- if (content) {
4241
+ if (content || usage) {
4162
4242
  yield {
4163
4243
  text: content,
4164
4244
  tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens }