@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.
- package/package.json +1 -1
- package/src/graph.mjs +53 -16
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
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
|
-
}
|
|
468
|
-
|
|
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 {
|