@memstack/core 0.6.4 → 0.7.1

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
@@ -1,9 +1,13 @@
1
1
  # MemStack
2
2
 
3
+ > Implementation priority is maintained in the [canonical roadmap](docs/ROADMAP.md).
4
+
3
5
  > The open-source memory layer for AI agents — store, retrieve, summarize, and prune.
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/@memstack/core)](https://www.npmjs.com/package/@memstack/core)
8
+ [![CI](https://github.com/isiomaC/memstack/actions/workflows/ci.yml/badge.svg)](https://github.com/isiomaC/memstack/actions/workflows/ci.yml)
6
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![MCP Reference](https://img.shields.io/badge/MCP-LLM%20Reference-blue)](https://gitmcp.io/isiomaC/memstack)
7
11
 
8
12
  ```bash
9
13
  npm install @memstack/core
@@ -671,9 +675,16 @@ new OpenAIEmbeddingAdapter({ apiKey: "...", baseURL: "https://api.voyageai.com/v
671
675
 
672
676
  ### Storage Adapters
673
677
 
674
- MemStack ships with **11 production-ready storage adapters (7 experimental)** — every major backend, zero peer dependencies, all client-injected.
678
+ MemStack contains 18 storage-adapter implementations. Twelve are exported from `@memstack/core`; six remain experimental source implementations. Core has no runtime dependencies, and database clients are injected by callers.
679
+
680
+ **Support levels:**
675
681
 
676
- ### Production (e2e verified against real instances)
682
+ - **Production-ready** means exported from the public package, covered by unit tests, and supported as part of the public API.
683
+ - **Real-service E2E verified** means the adapter also passes against its actual database implementation in `pnpm test:e2e`.
684
+ - **Mock-tested** means unit coverage uses an injected fake client rather than a live cloud service.
685
+ - **Experimental** means implemented in source but not exported from the published package.
686
+
687
+ ### Public package exports
677
688
 
678
689
  **Built-in (zero external deps):**
679
690
  | Adapter | Backend | Use case |
@@ -687,6 +698,7 @@ MemStack ships with **11 production-ready storage adapters (7 experimental)**
687
698
  | Adapter | Backend | Vector search |
688
699
  |---|---|---|
689
700
  | `PostgresStorageAdapter` | PostgreSQL + pgvector | HNSW native |
701
+ | `SQLiteStorageAdapter` | SQLite (better-sqlite3) | Cosine in-memory |
690
702
 
691
703
  **Vector databases:**
692
704
  | Adapter | Backend |
@@ -706,13 +718,12 @@ MemStack ships with **11 production-ready storage adapters (7 experimental)**
706
718
  |---|---|
707
719
  | `Neo4jStorageAdapter` | Neo4j |
708
720
 
709
- ### Experimental (mock-tested, blocked by cloud deps or platform constraints)
721
+ ### Experimental (mock-tested or missing an optional E2E capability)
710
722
 
711
- Available via direct source import. Not yet in the barrel export — uncomment in `src/index.ts` when e2e verified.
723
+ These implementations are available to source contributors but are not part of the published package API.
712
724
 
713
725
  | Adapter | Backend | Blocker |
714
726
  |---|---|---|
715
- | `SQLiteStorageAdapter` | SQLite (better-sqlite3) | Native binary for Node 24 |
716
727
  | `TursoStorageAdapter` | Turso (libsql) | Cloud-only (needs Turso account) |
717
728
  | `ChromaStorageAdapter` | ChromaDB | Embedding function dependency |
718
729
  | `PineconeStorageAdapter` | Pinecone | Cloud-only (needs API key) |
@@ -720,7 +731,7 @@ Available via direct source import. Not yet in the barrel export — uncomment i
720
731
  | `Mem0StorageAdapter` | Mem0 OSS or Cloud | Cloud-only (needs API key) |
721
732
  | `ZepStorageAdapter` | Zep Cloud or CE | Cloud-only (needs API key) |
722
733
 
723
- > **Direct import:** `import { ChromaStorageAdapter } from "@memstack/core/src/adapters/storage/chroma.js"`
734
+ Live cloud compatibility remains unverified for Pinecone, Upstash, Mem0, Zep, and Turso. Chroma's real-client E2E suite is skipped when its optional default embedding function is unavailable. LLM and embedding-provider tests use mocks; live-provider testing is opt-in and is not part of CI.
724
735
 
725
736
  **Quick-start per backend:**
726
737
 
@@ -781,7 +792,6 @@ class MyStorage implements StorageProvider {
781
792
  | Neo4j | Neo4j vector index | No | ✅ Production |
782
793
  | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
783
794
  | SQLite | Cosine in-memory | Yes | ✅ Production |
784
- | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
785
795
 
786
796
  ---
787
797
 
@@ -797,18 +807,28 @@ const ms = new MemStack({
797
807
  embedding?: EmbeddingProvider, // Optional — for semantic search
798
808
  storage?: StorageProvider, // Optional — defaults to InMemoryStorageAdapter
799
809
  defaults?: {
800
- summarizationThreshold?: number, // Auto-summarize every N interactions. Default: 100
810
+ summarizationThreshold?: number, // Auto-summarize every N process() calls. Default: 100
801
811
  embedOnStore?: boolean, // Auto-embed on store(). Default: true
802
- pruneStrategy?: PruneStrategy, // Auto-prune on every store(). Default: disabled
812
+ pruneStrategy?: PruneStrategy, // Auto-prune during process() (throttled). Default: disabled
813
+ pruneInterval?: number, // Run auto-prune every N process() calls. Default: 100
814
+ autoImportance?: boolean, // LLM-score importance in process() when not provided. Default: false
815
+ autoTags?: boolean, // LLM-extract tags in process() when not provided. Default: false
816
+ summarizationPrompt?: string, // Custom prompt for the summarizer
803
817
  },
804
818
  hooks?: {
805
819
  onMemoryStored?: (memory: Memory) => void;
806
820
  onMemoryPruned?: (ids: string[]) => void;
807
821
  onSummaryCreated?: (summary: Memory, deletedCount: number) => void;
822
+ onError?: (error: Error, context: string) => void;
808
823
  },
809
824
  });
810
825
  ```
811
826
 
827
+ > **Auto-behaviors run inside `process()`, not `store()`.** `process()` tracks a
828
+ > per-actor call count: summarization fires every `summarizationThreshold` calls,
829
+ > and pruning fires every `pruneInterval` calls (when `pruneStrategy` is set).
830
+ > `store()` is the low-level write and never triggers these.
831
+
812
832
  ### Memory Subsystem
813
833
 
814
834
  All methods accessible via `ms.memory.*`:
@@ -857,6 +877,8 @@ const data = JSON.parse(fs.readFileSync("state.json", "utf-8"));
857
877
  await ms2.import(data);
858
878
  ```
859
879
 
880
+ 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.
881
+
860
882
  ### Health & Close
861
883
 
862
884
  ```typescript
@@ -874,14 +896,17 @@ await ms.close(); // graceful shutdown
874
896
  const ms = new MemStack({
875
897
  llm: new OpenAILLMAdapter({ apiKey: "..." }),
876
898
 
877
- // Defaults control auto-behavior
899
+ // Defaults control auto-behavior (all applied during process())
878
900
  defaults: {
879
- summarizationThreshold: 50, // Summarize every 50 interactions (default: 100)
901
+ summarizationThreshold: 50, // Summarize every 50 process() calls (default: 100)
880
902
  embedOnStore: false, // Don't auto-embed — saves API costs
881
- pruneStrategy: { // Auto-clean on every store()
903
+ pruneStrategy: { // Auto-clean during process(), throttled by pruneInterval
882
904
  type: "byAge",
883
905
  maxAge: 90 * 86400000, // 90 days
884
906
  },
907
+ pruneInterval: 100, // Run the prune check every 100 process() calls (default: 100)
908
+ autoImportance: true, // Let the LLM score importance when you don't pass one
909
+ autoTags: true, // Let the LLM extract tags when you don't pass any
885
910
  },
886
911
 
887
912
  // Hooks for observability
@@ -889,6 +914,7 @@ const ms = new MemStack({
889
914
  onMemoryStored: (m) => logger.debug("memory:stored", { id: m.id, actor: m.actorId }),
890
915
  onMemoryPruned: (ids) => logger.info("memory:pruned", { count: ids.length }),
891
916
  onSummaryCreated: (summary, n) => logger.info("memory:summarized", { count: n }),
917
+ onError: (err, context) => logger.error("memory:error", { context, message: err.message }),
892
918
  },
893
919
  });
894
920
  ```
@@ -948,13 +974,21 @@ git clone https://github.com/isiomaC/memstack.git
948
974
  cd memstack
949
975
  pnpm install
950
976
 
951
- pnpm test # 393 tests, no external services needed
952
- pnpm test:e2e # 82 E2E tests (requires Docker)
953
- pnpm test:watch # Watch mode
954
- pnpm build # CJS + ESM + type declarations
955
- pnpm check # TypeScript type-check only
977
+ pnpm test # 407 core tests, no external services needed
978
+ pnpm test:packages # 78 package tests after dependency-ordered builds
979
+ pnpm test:e2e # 80 pass, 1 optional Chroma skip (requires Docker services)
980
+ pnpm test:e2e:run # Start services, run E2E once, preserve failure logs, clean up
981
+ pnpm smoke:artifacts # Built core, CLI, MCP, and server black-box checks
982
+ pnpm smoke:packages # Pack and install publishable tarballs in a clean project
983
+ pnpm smoke:docker # Build and exercise the server image
984
+ pnpm verify # Complete local verification pipeline
985
+ pnpm test:watch # Watch core tests
986
+ pnpm build:all # Build core and all workspace packages
987
+ pnpm check:all # Type-check core and all workspace packages
956
988
  ```
957
989
 
990
+ CI exposes a stable `verification` job. Configure that job as a required status check in GitHub branch protection for `main`.
991
+
958
992
  ### Debugging
959
993
 
960
994
  Use hooks for observability — MemStack has no built-in logging:
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) {
@@ -3150,7 +3221,7 @@ var LanceDBStorageAdapter = class {
3150
3221
  return results;
3151
3222
  }
3152
3223
  async get(id) {
3153
- const results = await this.table.search(this._zeroVector).limit(1).where(`id = "${id}"`).execute();
3224
+ const results = await this.table.search(this._zeroVector).limit(1).where(`id = "${id}"`).prefilter(true).execute();
3154
3225
  if (results.length === 0) return null;
3155
3226
  const row = results[0];
3156
3227
  if (row.expires_at) {
@@ -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 }