@jeremiaheth/neolata-mem 0.5.0 → 0.5.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/graph.mjs +53 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeremiaheth/neolata-mem",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "Graph-native memory engine for AI agents with Zettelkasten linking, biological decay, and conflict resolution",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
package/src/graph.mjs CHANGED
@@ -81,6 +81,8 @@ export class MemoryGraph {
81
81
  maxMemories: config.maxMemories ?? 50000,
82
82
  maxMemoryLength: config.maxMemoryLength ?? 10000,
83
83
  maxAgentLength: config.maxAgentLength ?? 64,
84
+ maxBatchSize: config.maxBatchSize ?? 1000,
85
+ maxQueryBatchSize: config.maxQueryBatchSize ?? 100,
84
86
  evolveMinIntervalMs: config.evolveMinIntervalMs ?? 1000,
85
87
  };
86
88
  }
@@ -384,6 +386,7 @@ export class MemoryGraph {
384
386
  if (agent.length > this.config.maxAgentLength) throw new Error(`agent exceeds max length (${this.config.maxAgentLength})`);
385
387
  if (!/^[a-zA-Z0-9_\-. ]+$/.test(agent)) throw new Error('agent contains invalid characters');
386
388
  if (!Array.isArray(items) || items.length === 0) throw new Error('items must be a non-empty array');
389
+ if (items.length > this.config.maxBatchSize) throw new Error(`Batch of ${items.length} exceeds max batch size (${this.config.maxBatchSize})`);
387
390
 
388
391
  await this.init();
389
392
 
@@ -399,7 +402,7 @@ export class MemoryGraph {
399
402
  return text;
400
403
  });
401
404
 
402
- // Batch embed all texts
405
+ // Batch embed all texts (before mutating state)
403
406
  const allEmbeddings = [];
404
407
  for (let i = 0; i < texts.length; i += embeddingBatchSize) {
405
408
  const batch = texts.slice(i, i + embeddingBatchSize);
@@ -407,6 +410,9 @@ export class MemoryGraph {
407
410
  allEmbeddings.push(...embeddings);
408
411
  }
409
412
 
413
+ // Build all new memories + backlink mutations before committing
414
+ const newMems = [];
415
+ const backlinkAdded = []; // track {target, linkEntry} for rollback
410
416
  const results = [];
411
417
  const now = new Date().toISOString();
412
418
 
@@ -424,6 +430,14 @@ export class MemoryGraph {
424
430
  related.push({ id: existing.id, similarity: sim, agent: existing.agent });
425
431
  }
426
432
  }
433
+ // Also check already-staged new mems in this batch
434
+ for (const staged of newMems) {
435
+ if (!staged.embedding) continue;
436
+ const sim = cosineSimilarity(embedding, staged.embedding);
437
+ if (sim > this.config.linkThreshold) {
438
+ related.push({ id: staged.id, similarity: sim, agent: staged.agent });
439
+ }
440
+ }
427
441
  related.sort((a, b) => b.similarity - a.similarity);
428
442
  }
429
443
  const topLinks = related.slice(0, this.config.maxLinksPerMemory);
@@ -438,34 +452,54 @@ export class MemoryGraph {
438
452
  links: topLinks.map(l => ({ id: l.id, similarity: l.similarity })),
439
453
  created_at: now, updated_at: now,
440
454
  };
455
+ newMems.push(newMem);
456
+ results.push({ id, links: topLinks.length });
457
+ }
441
458
 
459
+ // Commit phase: push all to memory + indexes, add backlinks
460
+ for (const newMem of newMems) {
442
461
  this.memories.push(newMem);
443
462
  this._indexMemory(newMem);
444
463
 
445
- // Backlinks
446
- for (const link of topLinks) {
464
+ for (const link of newMem.links) {
447
465
  const target = this._byId(link.id);
448
466
  if (target) {
449
467
  if (!target.links) target.links = [];
450
- if (!target.links.find(l => l.id === id)) {
451
- target.links.push({ id, similarity: link.similarity });
468
+ if (!target.links.find(l => l.id === newMem.id)) {
469
+ const linkEntry = { id: newMem.id, similarity: link.similarity };
470
+ target.links.push(linkEntry);
471
+ target.updated_at = now;
472
+ backlinkAdded.push({ target, linkEntry });
452
473
  }
453
- target.updated_at = now;
454
474
  }
455
475
  }
456
-
457
- results.push({ id, links: topLinks.length });
458
- this.emit('store', { id, agent, content: newMem.memory, category: newMem.category, importance: newMem.importance, links: topLinks.length });
459
476
  }
460
477
 
461
- // Single save at the end
462
- if (this.storage.incremental) {
463
- for (const r of results) {
464
- const mem = this._byId(r.id);
465
- if (mem) await this.storage.upsert(mem);
478
+ // Persist rollback on failure
479
+ try {
480
+ if (this.storage.incremental) {
481
+ for (const newMem of newMems) {
482
+ await this.storage.upsert(newMem);
483
+ }
484
+ } else {
485
+ await this.save();
466
486
  }
467
- } else {
468
- await this.save();
487
+ } catch (err) {
488
+ // Rollback: remove new memories from state + indexes
489
+ const newIds = new Set(newMems.map(m => m.id));
490
+ for (const newMem of newMems) this._deindexMemory(newMem);
491
+ this.memories = this.memories.filter(m => !newIds.has(m.id));
492
+ // Rollback backlinks
493
+ for (const { target, linkEntry } of backlinkAdded) {
494
+ target.links = (target.links || []).filter(l => l !== linkEntry);
495
+ }
496
+ throw err;
497
+ }
498
+
499
+ // Emit events only after successful persist
500
+ for (let i = 0; i < newMems.length; i++) {
501
+ const m = newMems[i];
502
+ this.emit('store', { id: m.id, agent, content: m.memory, category: m.category, importance: m.importance, links: results[i].links });
469
503
  }
470
504
 
471
505
  return { total: items.length, stored: results.length, results };
@@ -482,6 +516,7 @@ export class MemoryGraph {
482
516
  */
483
517
  async searchMany(agent, queries, { limit = 10, minSimilarity = 0 } = {}) {
484
518
  if (!Array.isArray(queries) || queries.length === 0) throw new Error('queries must be a non-empty array');
519
+ if (queries.length > this.config.maxQueryBatchSize) throw new Error(`${queries.length} queries exceeds max query batch size (${this.config.maxQueryBatchSize})`);
485
520
 
486
521
  await this.init();
487
522
 
@@ -973,6 +1008,7 @@ Respond ONLY with a JSON object:
973
1008
  const existing = this._byId(update.memoryId);
974
1009
  if (existing) {
975
1010
  const oldContent = existing.memory;
1011
+ this._deindexMemory(existing);
976
1012
  existing.memory = text;
977
1013
  existing.updated_at = new Date().toISOString();
978
1014
  existing.importance = Math.max(existing.importance, importance);
@@ -980,6 +1016,7 @@ Respond ONLY with a JSON object:
980
1016
  existing.embedding = newEmb;
981
1017
  existing.evolution = existing.evolution || [];
982
1018
  existing.evolution.push({ from: oldContent, to: text, reason: update.reason, at: new Date().toISOString() });
1019
+ this._indexMemory(existing);
983
1020
  if (this.storage.incremental) {
984
1021
  await this.storage.upsert(existing);
985
1022
  } else {