@memstack/core 0.6.4 → 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/dist/index.d.cts CHANGED
@@ -64,6 +64,7 @@ interface MemoryStoreInput {
64
64
  sourceId?: string;
65
65
  metadata?: Record<string, unknown>;
66
66
  expiresAt?: Date;
67
+ createdAt?: Date;
67
68
  onConflict?: "append";
68
69
  /** Pre-assigned ID. When set, the storage adapter uses it instead of generating one. */
69
70
  id?: string;
@@ -106,6 +107,7 @@ interface SummarizeOptions {
106
107
  }
107
108
  interface PruneStrategy {
108
109
  type: "byAge" | "byImportance" | "byCount" | "byType" | "custom" | "compose";
110
+ actorId?: string;
109
111
  maxAge?: number;
110
112
  minImportance?: number;
111
113
  maxPerActor?: number;
@@ -333,6 +335,13 @@ declare class DiskStorageAdapter implements StorageProvider {
333
335
  private _readFile;
334
336
  private _writeFile;
335
337
  private _withWriteLock;
338
+ /**
339
+ * Acquires an exclusive cross-process lock for `actorId` via `open(path, "wx")`
340
+ * (atomic create-if-absent). Retries with jittered backoff while the lock is
341
+ * held elsewhere, and steals locks left behind by a crashed process once
342
+ * they're older than `staleMs`.
343
+ */
344
+ private _acquireFileLock;
336
345
  private _listActors;
337
346
  private _buildIndex;
338
347
  private _toExternal;
package/dist/index.d.ts CHANGED
@@ -64,6 +64,7 @@ interface MemoryStoreInput {
64
64
  sourceId?: string;
65
65
  metadata?: Record<string, unknown>;
66
66
  expiresAt?: Date;
67
+ createdAt?: Date;
67
68
  onConflict?: "append";
68
69
  /** Pre-assigned ID. When set, the storage adapter uses it instead of generating one. */
69
70
  id?: string;
@@ -106,6 +107,7 @@ interface SummarizeOptions {
106
107
  }
107
108
  interface PruneStrategy {
108
109
  type: "byAge" | "byImportance" | "byCount" | "byType" | "custom" | "compose";
110
+ actorId?: string;
109
111
  maxAge?: number;
110
112
  minImportance?: number;
111
113
  maxPerActor?: number;
@@ -333,6 +335,13 @@ declare class DiskStorageAdapter implements StorageProvider {
333
335
  private _readFile;
334
336
  private _writeFile;
335
337
  private _withWriteLock;
338
+ /**
339
+ * Acquires an exclusive cross-process lock for `actorId` via `open(path, "wx")`
340
+ * (atomic create-if-absent). Retries with jittered backoff while the lock is
341
+ * held elsewhere, and steals locks left behind by a crashed process once
342
+ * they're older than `staleMs`.
343
+ */
344
+ private _acquireFileLock;
336
345
  private _listActors;
337
346
  private _buildIndex;
338
347
  private _toExternal;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile, readdir, appendFile } from 'fs/promises';
1
+ import { mkdir, readFile, writeFile, rename, unlink, open, stat, readdir, appendFile } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
 
4
4
  // src/errors.ts
@@ -598,7 +598,7 @@ var MemoryStore = class {
598
598
  }
599
599
  async prune(strategy) {
600
600
  await this._ensureInit();
601
- const allMemories = await this.storage.retrieve({ limit: this.limits.pruneScan });
601
+ const allMemories = await this.storage.retrieve({ actorId: strategy.actorId, limit: this.limits.pruneScan });
602
602
  const kept = this.pruner.execute(allMemories, strategy);
603
603
  const keptIds = new Set(kept.map((k) => k.id));
604
604
  const prunedIds = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
@@ -610,7 +610,7 @@ var MemoryStore = class {
610
610
  }
611
611
  async dryRunPrune(strategy) {
612
612
  await this._ensureInit();
613
- const allMemories = await this.storage.retrieve({ limit: this.limits.pruneScan });
613
+ const allMemories = await this.storage.retrieve({ actorId: strategy.actorId, limit: this.limits.pruneScan });
614
614
  const kept = this.pruner.execute(allMemories, strategy);
615
615
  const keptIds = new Set(kept.map((k) => k.id));
616
616
  const wouldPrune = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
@@ -744,7 +744,7 @@ var InMemoryStorageAdapter = class {
744
744
  sourceId: input.sourceId,
745
745
  metadata: input.metadata ?? {},
746
746
  expiresAt: input.expiresAt,
747
- createdAt: now,
747
+ createdAt: input.createdAt ?? now,
748
748
  _touchedAt: now
749
749
  };
750
750
  this.memories.set(memory.id, memory);
@@ -794,8 +794,11 @@ var InMemoryStorageAdapter = class {
794
794
  results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
795
795
  }
796
796
  if (query.query) {
797
- const q = query.query.toLowerCase();
798
- results = results.filter((m) => m.content.toLowerCase().includes(q));
797
+ const terms = query.query.toLowerCase().split(/\s+/).filter(Boolean);
798
+ results = results.filter((m) => {
799
+ const content = m.content.toLowerCase();
800
+ return terms.some((t) => content.includes(t));
801
+ });
799
802
  }
800
803
  if (query.createdAfter) {
801
804
  results = results.filter((m) => m.createdAt >= query.createdAfter);
@@ -981,7 +984,19 @@ var MemStack = class {
981
984
  if (snapshot.version !== 1) {
982
985
  throw validationError(`Unsupported snapshot version: ${snapshot.version}`);
983
986
  }
984
- await this.memory.storeBatch(snapshot.memories);
987
+ const memories = snapshot.memories.map((m, i) => {
988
+ const createdAt = typeof m.createdAt === "string" ? new Date(m.createdAt) : m.createdAt;
989
+ const expiresAt = typeof m.expiresAt === "string" ? new Date(m.expiresAt) : m.expiresAt;
990
+ const label = m.id ? `id: ${m.id}` : `index ${i}`;
991
+ if (createdAt && isNaN(createdAt.getTime())) {
992
+ throw validationError(`Invalid createdAt for memory (${label}): ${String(m.createdAt)}`);
993
+ }
994
+ if (expiresAt && isNaN(expiresAt.getTime())) {
995
+ throw validationError(`Invalid expiresAt for memory (${label}): ${String(m.expiresAt)}`);
996
+ }
997
+ return { ...m, createdAt, expiresAt };
998
+ });
999
+ await this.memory.storeBatch(memories);
985
1000
  }
986
1001
  async health() {
987
1002
  const status = { storage: false, llm: false, embedding: false };
@@ -1068,7 +1083,7 @@ var DiskStorageAdapter = class {
1068
1083
  sourceId: input.sourceId,
1069
1084
  metadata: input.metadata ?? {},
1070
1085
  expiresAt: input.expiresAt,
1071
- createdAt: now,
1086
+ createdAt: input.createdAt ?? now,
1072
1087
  _touchedAt: now.toISOString()
1073
1088
  };
1074
1089
  await this._withWriteLock(input.actorId, async () => {
@@ -1101,7 +1116,7 @@ var DiskStorageAdapter = class {
1101
1116
  sourceId: input.sourceId,
1102
1117
  metadata: input.metadata ?? {},
1103
1118
  expiresAt: input.expiresAt,
1104
- createdAt: now,
1119
+ createdAt: input.createdAt ?? now,
1105
1120
  _touchedAt: now.toISOString()
1106
1121
  };
1107
1122
  all.push(memory);
@@ -1208,8 +1223,11 @@ var DiskStorageAdapter = class {
1208
1223
  records = records.filter((r) => r.tags?.some((t) => query.tags.includes(t)));
1209
1224
  }
1210
1225
  if (query.query) {
1211
- const q = query.query.toLowerCase();
1212
- records = records.filter((r) => r.content.toLowerCase().includes(q));
1226
+ const terms = query.query.toLowerCase().split(/\s+/).filter(Boolean);
1227
+ records = records.filter((r) => {
1228
+ const content = r.content.toLowerCase();
1229
+ return terms.some((t) => content.includes(t));
1230
+ });
1213
1231
  }
1214
1232
  results.push(...records);
1215
1233
  }
@@ -1300,9 +1318,14 @@ var DiskStorageAdapter = class {
1300
1318
  }
1301
1319
  }
1302
1320
  async _writeFile(actorId, records) {
1321
+ const filePath = this._filePath(actorId);
1322
+ const tmpPath = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
1303
1323
  try {
1304
- await writeFile(this._filePath(actorId), JSON.stringify(records, null, 2), "utf-8");
1324
+ await writeFile(tmpPath, JSON.stringify(records, null, 2), "utf-8");
1325
+ await rename(tmpPath, filePath);
1305
1326
  } catch (err) {
1327
+ await unlink(tmpPath).catch(() => {
1328
+ });
1306
1329
  throw storageError(`Failed to write memories for actor ${actorId}: ${err.message}`);
1307
1330
  }
1308
1331
  }
@@ -1314,15 +1337,57 @@ var DiskStorageAdapter = class {
1314
1337
  });
1315
1338
  this._writeLocks.set(actorId, next);
1316
1339
  await prev;
1340
+ const releaseFileLock = await this._acquireFileLock(actorId);
1317
1341
  try {
1318
1342
  await fn();
1319
1343
  } finally {
1344
+ await releaseFileLock();
1320
1345
  resolve();
1321
1346
  if (this._writeLocks.get(actorId) === next) {
1322
1347
  this._writeLocks.delete(actorId);
1323
1348
  }
1324
1349
  }
1325
1350
  }
1351
+ /**
1352
+ * Acquires an exclusive cross-process lock for `actorId` via `open(path, "wx")`
1353
+ * (atomic create-if-absent). Retries with jittered backoff while the lock is
1354
+ * held elsewhere, and steals locks left behind by a crashed process once
1355
+ * they're older than `staleMs`.
1356
+ */
1357
+ async _acquireFileLock(actorId) {
1358
+ const lockPath = `${this._filePath(actorId)}.lock`;
1359
+ const staleMs = 1e4;
1360
+ const timeoutMs = 3e4;
1361
+ const start = Date.now();
1362
+ while (true) {
1363
+ try {
1364
+ const handle = await open(lockPath, "wx");
1365
+ await handle.close();
1366
+ return async () => {
1367
+ await unlink(lockPath).catch(() => {
1368
+ });
1369
+ };
1370
+ } catch (err) {
1371
+ if (err.code !== "EEXIST") {
1372
+ throw storageError(`Failed to acquire lock for actor ${actorId}: ${err.message}`);
1373
+ }
1374
+ try {
1375
+ const st = await stat(lockPath);
1376
+ if (Date.now() - st.mtimeMs > staleMs) {
1377
+ await unlink(lockPath).catch(() => {
1378
+ });
1379
+ continue;
1380
+ }
1381
+ } catch {
1382
+ continue;
1383
+ }
1384
+ if (Date.now() - start > timeoutMs) {
1385
+ throw storageError(`Timed out waiting for disk storage lock: ${lockPath}`);
1386
+ }
1387
+ await new Promise((r) => setTimeout(r, 15 + Math.random() * 35));
1388
+ }
1389
+ }
1390
+ }
1326
1391
  async _listActors() {
1327
1392
  try {
1328
1393
  const entries = await readdir(this.dir);
@@ -1380,7 +1445,7 @@ var MarkdownStorageAdapter = class {
1380
1445
  sourceId: input.sourceId,
1381
1446
  metadata: input.metadata ?? {},
1382
1447
  expiresAt: input.expiresAt,
1383
- createdAt: now
1448
+ createdAt: input.createdAt ?? now
1384
1449
  };
1385
1450
  const filePath = this._filePath(input.actorId);
1386
1451
  const block = this._formatBlock(memory);
@@ -1457,8 +1522,11 @@ var MarkdownStorageAdapter = class {
1457
1522
  results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
1458
1523
  }
1459
1524
  if (query.query) {
1460
- const q = query.query.toLowerCase();
1461
- results = results.filter((m) => m.content.toLowerCase().includes(q));
1525
+ const terms = query.query.toLowerCase().split(/\s+/).filter(Boolean);
1526
+ results = results.filter((m) => {
1527
+ const content = m.content.toLowerCase();
1528
+ return terms.some((t) => content.includes(t));
1529
+ });
1462
1530
  }
1463
1531
  switch (query.strategy) {
1464
1532
  case "recent":
@@ -2009,7 +2077,7 @@ var PostgresStorageAdapter = class {
2009
2077
  sourceId: input.sourceId,
2010
2078
  metadata: input.metadata ?? {},
2011
2079
  expiresAt: input.expiresAt,
2012
- createdAt: new Date(now)
2080
+ createdAt: input.createdAt ?? new Date(now)
2013
2081
  };
2014
2082
  }
2015
2083
  async storeBatch(inputs) {
@@ -2212,7 +2280,7 @@ var RedisStorageAdapter = class {
2212
2280
  sourceId: input.sourceId,
2213
2281
  metadata: input.metadata ?? {},
2214
2282
  expiresAt: input.expiresAt,
2215
- createdAt: now,
2283
+ createdAt: input.createdAt ?? now,
2216
2284
  _touchedAt: now.toISOString()
2217
2285
  };
2218
2286
  await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
@@ -2236,7 +2304,7 @@ var RedisStorageAdapter = class {
2236
2304
  sourceId: input.sourceId,
2237
2305
  metadata: input.metadata ?? {},
2238
2306
  expiresAt: input.expiresAt,
2239
- createdAt: now,
2307
+ createdAt: input.createdAt ?? now,
2240
2308
  _touchedAt: now.toISOString()
2241
2309
  };
2242
2310
  await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
@@ -2432,7 +2500,7 @@ var QdrantStorageAdapter = class {
2432
2500
  sourceId: input.sourceId,
2433
2501
  metadata: input.metadata ?? {},
2434
2502
  expiresAt: input.expiresAt,
2435
- createdAt: now
2503
+ createdAt: input.createdAt ?? now
2436
2504
  };
2437
2505
  const qdrantId = crypto.randomUUID();
2438
2506
  await this.client.upsert(this.collectionName, {
@@ -2464,7 +2532,7 @@ var QdrantStorageAdapter = class {
2464
2532
  sourceId: input.sourceId,
2465
2533
  metadata: input.metadata ?? {},
2466
2534
  expiresAt: input.expiresAt,
2467
- createdAt: now
2535
+ createdAt: input.createdAt ?? now
2468
2536
  };
2469
2537
  results.push(memory);
2470
2538
  const qid = crypto.randomUUID();
@@ -2642,10 +2710,13 @@ var Neo4jStorageAdapter = class {
2642
2710
  sourceId: input.sourceId ?? null,
2643
2711
  metadata: JSON.stringify(input.metadata ?? {}),
2644
2712
  expiresAt: input.expiresAt?.toISOString() ?? null,
2645
- createdAt: now,
2713
+ createdAt: input.createdAt?.toISOString() ?? now,
2646
2714
  embedding: input.embedding ?? null
2647
2715
  };
2648
- const { createdAt: _, ...updateProps } = props;
2716
+ const updateProps = { ...props };
2717
+ if (!input.createdAt) {
2718
+ delete updateProps.createdAt;
2719
+ }
2649
2720
  const session = this.driver.session({ database: this.database });
2650
2721
  try {
2651
2722
  await session.run(
@@ -2667,7 +2738,7 @@ var Neo4jStorageAdapter = class {
2667
2738
  sourceId: input.sourceId,
2668
2739
  metadata: input.metadata ?? {},
2669
2740
  expiresAt: input.expiresAt,
2670
- createdAt: new Date(now)
2741
+ createdAt: input.createdAt ?? new Date(now)
2671
2742
  };
2672
2743
  }
2673
2744
  async storeBatch(inputs) {
@@ -2904,7 +2975,7 @@ var WeaviateStorageAdapter = class {
2904
2975
  sourceId: input.sourceId,
2905
2976
  metadata: input.metadata ?? {},
2906
2977
  expiresAt: input.expiresAt,
2907
- createdAt: now
2978
+ createdAt: input.createdAt ?? now
2908
2979
  };
2909
2980
  const wvId = await this.collection().data.insert({
2910
2981
  id: crypto.randomUUID(),
@@ -3137,7 +3208,7 @@ var LanceDBStorageAdapter = class {
3137
3208
  sourceId: input.sourceId,
3138
3209
  metadata: input.metadata ?? {},
3139
3210
  expiresAt: input.expiresAt,
3140
- createdAt: now
3211
+ createdAt: input.createdAt ?? now
3141
3212
  };
3142
3213
  }
3143
3214
  async storeBatch(inputs) {
@@ -3305,7 +3376,7 @@ var MongoDBStorageAdapter = class {
3305
3376
  sourceId: input.sourceId,
3306
3377
  metadata: input.metadata ?? {},
3307
3378
  expiresAt: input.expiresAt,
3308
- createdAt: now
3379
+ createdAt: input.createdAt ?? now
3309
3380
  };
3310
3381
  await this.collection.insertOne(this.toDoc(memory));
3311
3382
  return memory;
@@ -3534,7 +3605,7 @@ var SQLiteStorageAdapter = class {
3534
3605
  sourceId: input.sourceId,
3535
3606
  metadata: input.metadata ?? {},
3536
3607
  expiresAt: input.expiresAt,
3537
- createdAt: new Date(now)
3608
+ createdAt: input.createdAt ?? new Date(now)
3538
3609
  };
3539
3610
  }
3540
3611
  async storeBatch(inputs) {
@@ -3807,11 +3878,12 @@ var OpenAILLMAdapter = class {
3807
3878
  try {
3808
3879
  const parsed = JSON.parse(data);
3809
3880
  const content = parsed.choices?.[0]?.delta?.content ?? "";
3810
- if (parsed.usage) {
3811
- promptTokens = parsed.usage.prompt_tokens;
3812
- completionTokens = parsed.usage.completion_tokens;
3881
+ const usage = parsed.usage;
3882
+ if (usage) {
3883
+ promptTokens = usage.prompt_tokens;
3884
+ completionTokens = usage.completion_tokens;
3813
3885
  }
3814
- if (content) {
3886
+ if (content || usage) {
3815
3887
  yield {
3816
3888
  text: content,
3817
3889
  tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens }
@@ -3867,7 +3939,15 @@ var AnthropicLLMAdapter = class {
3867
3939
  };
3868
3940
  } catch (err) {
3869
3941
  if (err instanceof MemStackError) throw err;
3870
- throw new MemStackError("LLM_ERROR", `Anthropic request failed: ${err instanceof Error ? err.message : String(err)}`, {
3942
+ const msg = err instanceof Error ? err.message : String(err);
3943
+ const code = err?.code;
3944
+ 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));
3945
+ if (isMissingSdk) {
3946
+ throw new MemStackError("LLM_ERROR", "Anthropic adapter requires @anthropic-ai/sdk. Install it: npm install @anthropic-ai/sdk", {
3947
+ retryable: false
3948
+ });
3949
+ }
3950
+ throw new MemStackError("LLM_ERROR", `Anthropic request failed: ${msg}`, {
3871
3951
  retryable: true
3872
3952
  });
3873
3953
  }
@@ -4156,7 +4236,7 @@ var GroqLLMAdapter = class {
4156
4236
  promptTokens = usage.prompt_tokens;
4157
4237
  completionTokens = usage.completion_tokens;
4158
4238
  }
4159
- if (content) {
4239
+ if (content || usage) {
4160
4240
  yield {
4161
4241
  text: content,
4162
4242
  tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens }