@jeremiaheth/neolata-mem 0.8.2 → 0.8.6

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
@@ -520,20 +520,57 @@ Decay Cycle:
520
520
  |---------|------------|------|-------|-----|
521
521
  | Zettelkasten linking | ✅ | ❌ | ❌ | ❌ |
522
522
  | Biological decay | ✅ | ❌ | ❌ | ❌ |
523
- | Graph traversal | ✅ | ❌ | ❌ | ✅ |
524
- | Multi-agent native | ✅ | | | ❌ |
525
- | Conflict resolution | ✅ | ✅ | ❌ | |
523
+ | Graph traversal | ✅ | ❌ | ❌ | ✅ ¹ |
524
+ | Multi-agent native | ✅ | ⚠️ ² | ⚠️ ³ | ❌ |
525
+ | Conflict resolution | ✅ | ✅ | ❌ | ⚠️ |
526
526
  | Quarantine lane | ✅ | ❌ | ❌ | ❌ |
527
- | Predicate schemas | ✅ | ❌ | ❌ | |
527
+ | Predicate schemas | ✅ | ❌ | ❌ | ⚠️ |
528
528
  | Runtime helpers (heartbeat/recall/dump) | ✅ | ❌ | ❌ | ❌ |
529
529
  | Explainability API | ✅ | ❌ | ❌ | ❌ |
530
- | Episodes & compression | ✅ | ❌ | ❌ | |
530
+ | Episodes & compression | ✅ | ❌ | ❌ | ⚠️ |
531
531
  | Labeled clusters | ✅ | ❌ | ❌ | ❌ |
532
532
  | Works offline | ✅ | ✅ | ✅ | ❌ |
533
- | No Python needed | ✅ | ❌ | ❌ | ❌ |
533
+ | No Python needed | ✅ | ❌ | ❌ | ❌ |
534
534
  | Zero-config start | ✅ | ❌ | ❌ | ❌ |
535
535
  | LLM optional | ✅ | ❌ | ❌ | ❌ |
536
536
 
537
+ <details>
538
+ <summary><b>Comparison notes</b> (click to expand)</summary>
539
+
540
+ 1. **Zep graph traversal** — Zep uses a temporal knowledge graph with entity/relation extraction. Different architecture from neolata-mem's Zettelkasten-style memory-to-memory links, but supports graph queries.
541
+ 2. **Mem0 multi-agent** — Mem0 supports User, Session, and Agent memory levels. Agent-scoped storage exists, but it's designed around user personalization rather than independent agent collaboration.
542
+ 3. **Letta multi-agent** — Letta supports shared memory blocks between agents and has a multi-agent orchestration layer. Not the same as neolata-mem's native agent-scoped store/search.
543
+ 4. **Zep conflict resolution** — Zep tracks temporal validity of facts (marking outdated facts as invalid when new info arrives). This is implicit conflict handling, not explicit quarantine/review like neolata-mem.
544
+ 5. **Zep predicate schemas** — Zep supports custom graph ontologies via Pydantic/Zod for domain-specific entity types. Different from neolata-mem's per-predicate conflict/dedup/normalization rules.
545
+ 6. **Zep episodes** — Zep structures interactions into episodic sequences with temporal awareness. neolata-mem episodes are explicitly named groups with time-window capture, compression, and search.
546
+ 7. **Mem0 npm package** — Mem0 offers an npm SDK (`mem0ai`), but the core engine and self-hosted deployment require Python.
547
+
548
+ *Last updated: February 2026. Comparison based on publicly available documentation.*
549
+
550
+ </details>
551
+
552
+ ## Roadmap
553
+
554
+ ### v0.9 — Production Hardening (Q1 2026)
555
+ - [ ] **Supabase storage adapter** — first-class `storage: { type: 'supabase' }` config (currently requires custom wiring)
556
+ - [ ] **Write-ahead log** — crash recovery for JSON storage beyond atomic writes
557
+ - [ ] **Streaming search** — async iterator for large result sets
558
+ - [ ] **Memory import/export** — portable JSON/JSONL format for backup and migration
559
+ - [ ] **Benchmarks** — LOCOMO and LongMemEval comparison against Mem0, Zep, and raw vector stores
560
+
561
+ ### v1.0 — Stable API (Q2 2026)
562
+ - [ ] **API stabilization** — semantic versioning contract, deprecation policy
563
+ - [ ] **Plugin system** — bring-your-own storage/embeddings/LLM via a documented interface (beyond current provider pattern)
564
+ - [ ] **Web dashboard** — visual graph explorer, memory inspector, health monitor
565
+ - [ ] **Multi-backend sync** — replicate between JSON ↔ Supabase ↔ custom backends
566
+ - [ ] **Access control** — per-agent read/write permissions for shared deployments
567
+
568
+ ### Future
569
+ - [ ] **Real-time subscriptions** — watch for memory changes (useful for multi-agent coordination)
570
+ - [ ] **Temporal queries** — "what did agent X know about Y on date Z?"
571
+ - [ ] **Federation** — cross-instance memory sharing with trust boundaries
572
+ - [ ] **Browser SDK** — lightweight client for web apps (IndexedDB storage)
573
+
537
574
  ## Security
538
575
 
539
576
  neolata-mem includes several hardening measures:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeremiaheth/neolata-mem",
3
- "version": "0.8.2",
3
+ "version": "0.8.6",
4
4
  "description": "Trustworthy graph-native memory engine for AI agents - belief updates, provenance tracking, trust-gated supersession, and poisoning resistance",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
@@ -9,9 +9,14 @@
9
9
  "./graph": "./src/graph.mjs",
10
10
  "./embeddings": "./src/embeddings.mjs",
11
11
  "./storage": "./src/storage.mjs",
12
+ "./wal": "./src/wal.mjs",
13
+ "./wal-replay": "./src/wal-replay.mjs",
14
+ "./wal-snapshot": "./src/wal-snapshot.mjs",
15
+ "./wal-equivalence": "./src/wal-equivalence.mjs",
12
16
  "./supabase-storage": "./src/supabase-storage.mjs",
13
17
  "./extraction": "./src/extraction.mjs",
14
- "./llm": "./src/llm.mjs"
18
+ "./llm": "./src/llm.mjs",
19
+ "./wal-recovery": "./src/wal-recovery.mjs"
15
20
  },
16
21
  "bin": {
17
22
  "neolata-mem": "./cli/index.mjs"
@@ -19,14 +24,18 @@
19
24
  "files": [
20
25
  "src/",
21
26
  "cli/",
22
- "docs/*.md",
27
+ "docs/guide.md",
28
+ "docs/implementation-notes.md",
29
+ "docs/runtime-helpers.md",
23
30
  "LICENSE",
24
31
  "README.md"
25
32
  ],
26
33
  "scripts": {
27
34
  "test": "vitest run",
35
+ "validate:wal": "node --preserve-symlinks --preserve-symlinks-main scripts/validate-wal.mjs",
28
36
  "verify-skill": "node scripts/verify-skill.mjs",
29
- "prepublishOnly": "vitest run && node scripts/verify-skill.mjs"
37
+ "prepublishOnly": "vitest run && node scripts/verify-skill.mjs",
38
+ "validate:recovery": "node --preserve-symlinks --preserve-symlinks-main scripts/validate-recovery.mjs"
30
39
  },
31
40
  "keywords": [
32
41
  "ai",
@@ -52,7 +61,7 @@
52
61
  "homepage": "https://github.com/Jeremiaheth/neolata-mem#readme",
53
62
  "repository": {
54
63
  "type": "git",
55
- "url": "https://github.com/Jeremiaheth/neolata-mem"
64
+ "url": "git+https://github.com/Jeremiaheth/neolata-mem.git"
56
65
  },
57
66
  "engines": {
58
67
  "node": ">=18.0.0"
package/src/graph.mjs CHANGED
@@ -14,7 +14,8 @@
14
14
  * - extraction: extract(text) → facts (optional)
15
15
  */
16
16
 
17
- import { cosineSimilarity } from './embeddings.mjs';
17
+ import { cosineSimilarity } from './embeddings.mjs';
18
+ import { createWalMutationEvent } from './wal.mjs';
18
19
 
19
20
  /** @typedef {{ id: string, agent: string, memory: string, category: string, importance: number, tags: string[], embedding: number[]|null, links: {id: string, similarity: number, type?: string}[], created_at: string, updated_at: string, evolution?: object[], accessCount?: number }} Memory */
20
21
 
@@ -207,11 +208,12 @@ export class MemoryGraph {
207
208
  * @param {number} [opts.config.archiveThreshold=0.15] - Strength below this → archive
208
209
  * @param {number} [opts.config.deleteThreshold=0.05] - Strength below this → delete
209
210
  */
210
- constructor({ storage, embeddings, extraction, llm, config = {} }) {
211
- this.storage = storage;
212
- this.embeddings = embeddings;
213
- this.extraction = extraction || null;
214
- this.llm = llm || null;
211
+ constructor({ storage, embeddings, extraction, llm, wal = null, config = {} }) {
212
+ this.storage = storage;
213
+ this.embeddings = embeddings;
214
+ this.extraction = extraction || null;
215
+ this.llm = llm || null;
216
+ this.wal = wal;
215
217
  this.memories = [];
216
218
  this.loaded = false;
217
219
  this._listeners = {};
@@ -314,11 +316,24 @@ export class MemoryGraph {
314
316
  }
315
317
 
316
318
  /** Emit an event to all registered listeners. */
317
- emit(event, data) {
318
- for (const fn of (this._listeners[event] || [])) {
319
- try { fn(data); } catch { /* listener errors don't break the engine */ }
320
- }
321
- }
319
+ emit(event, data) {
320
+ for (const fn of (this._listeners[event] || [])) {
321
+ try { fn(data); } catch { /* listener errors don't break the engine */ }
322
+ }
323
+ }
324
+
325
+ async _appendWal(op, { memoryId, actor = null, data = {} } = {}) {
326
+ if (!this.wal) return;
327
+ if (typeof this.wal.appendMutation === 'function') {
328
+ await this.wal.appendMutation({ op, memoryId, actor, data });
329
+ return;
330
+ }
331
+ if (typeof this.wal.append === 'function') {
332
+ await this.wal.append(createWalMutationEvent({ op, memoryId, actor, data }));
333
+ return;
334
+ }
335
+ throw new Error('wal backend must implement appendMutation() or append()');
336
+ }
322
337
 
323
338
  /** Load memories from storage (lazy, called once). */
324
339
  async init() {
@@ -697,10 +712,20 @@ export class MemoryGraph {
697
712
  } else {
698
713
  await this.save();
699
714
  }
700
- if (pendingConflictsChanged) await this._savePendingConflicts();
701
-
702
- // Emit store event
703
- this.emit('store', { id, agent, content: text, category, importance, links: topLinks.length });
715
+ if (pendingConflictsChanged) await this._savePendingConflicts();
716
+ await this._appendWal('store', {
717
+ memoryId: id,
718
+ actor: agent,
719
+ data: {
720
+ category,
721
+ importance,
722
+ status: newMem.status,
723
+ links: topLinks.length,
724
+ },
725
+ });
726
+
727
+ // Emit store event
728
+ this.emit('store', { id, agent, content: text, category, importance, links: topLinks.length });
704
729
 
705
730
  return {
706
731
  id,
@@ -1824,13 +1849,23 @@ export class MemoryGraph {
1824
1849
 
1825
1850
  mem.updated_at = now.toISOString();
1826
1851
 
1827
- if (this.storage.incremental) {
1828
- await this.storage.upsert(mem);
1829
- } else {
1830
- await this.save();
1831
- }
1832
-
1833
- const { strength } = this.calcStrength(mem);
1852
+ if (this.storage.incremental) {
1853
+ await this.storage.upsert(mem);
1854
+ } else {
1855
+ await this.save();
1856
+ }
1857
+ await this._appendWal('reinforce', {
1858
+ memoryId: mem.id,
1859
+ actor: mem.agent || null,
1860
+ data: {
1861
+ boost,
1862
+ importance: mem.importance,
1863
+ accessCount: mem.accessCount,
1864
+ reinforcements: mem.reinforcements,
1865
+ },
1866
+ });
1867
+
1868
+ const { strength } = this.calcStrength(mem);
1834
1869
  return {
1835
1870
  id: mem.id, memory: mem.memory, oldImportance, newImportance: mem.importance,
1836
1871
  accessCount: mem.accessCount, reinforcements: mem.reinforcements,
@@ -2027,10 +2062,20 @@ Respond ONLY with a JSON object:
2027
2062
 
2028
2063
  mem.updated_at = new Date().toISOString();
2029
2064
 
2030
- if (this.storage.incremental) await this.storage.upsert(mem);
2031
- else await this.save();
2032
-
2033
- this.emit('dispute', { id: memoryId, disputes: mem.disputes, trust: mem.provenance.trust, status: mem.status, reason });
2065
+ if (this.storage.incremental) await this.storage.upsert(mem);
2066
+ else await this.save();
2067
+ await this._appendWal('dispute', {
2068
+ memoryId,
2069
+ actor: mem.agent || null,
2070
+ data: {
2071
+ disputes: mem.disputes,
2072
+ trust: +mem.provenance.trust.toFixed(4),
2073
+ status: mem.status,
2074
+ reason: reason || null,
2075
+ },
2076
+ });
2077
+
2078
+ this.emit('dispute', { id: memoryId, disputes: mem.disputes, trust: mem.provenance.trust, status: mem.status, reason });
2034
2079
  return {
2035
2080
  id: memoryId, disputes: mem.disputes, trust: +mem.provenance.trust.toFixed(4),
2036
2081
  confidence: mem.confidence, status: mem.status,
@@ -2482,12 +2527,21 @@ Respond ONLY with a JSON object:
2482
2527
  const mem = this._byId(memoryId);
2483
2528
  if (!mem) throw new Error(`Memory not found: ${memoryId}`);
2484
2529
  if (mem.status !== 'active') throw new Error('Only active memories can be quarantined');
2485
- this._markQuarantined(mem, { reason, details });
2486
- mem.updated_at = new Date().toISOString();
2487
- if (this.storage.incremental) await this.storage.upsert(mem);
2488
- else await this.save();
2489
- return { id: mem.id, status: mem.status, quarantine: mem.quarantine };
2490
- }
2530
+ this._markQuarantined(mem, { reason, details });
2531
+ mem.updated_at = new Date().toISOString();
2532
+ if (this.storage.incremental) await this.storage.upsert(mem);
2533
+ else await this.save();
2534
+ await this._appendWal('quarantine', {
2535
+ memoryId: mem.id,
2536
+ actor: mem.agent || null,
2537
+ data: {
2538
+ reason,
2539
+ details: details || null,
2540
+ status: mem.status,
2541
+ },
2542
+ });
2543
+ return { id: mem.id, status: mem.status, quarantine: mem.quarantine };
2544
+ }
2491
2545
 
2492
2546
  async reviewQuarantine(memoryId, { action, reason } = {}) {
2493
2547
  await this.init();
package/src/index.mjs CHANGED
@@ -19,10 +19,11 @@
19
19
 
20
20
  import { MemoryGraph } from './graph.mjs';
21
21
  import { openaiEmbeddings, noopEmbeddings } from './embeddings.mjs';
22
- import { jsonStorage, memoryStorage } from './storage.mjs';
23
- import { supabaseStorage } from './supabase-storage.mjs';
24
- import { llmExtraction, passthroughExtraction } from './extraction.mjs';
25
- import { openaiChat, openclawChat } from './llm.mjs';
22
+ import { jsonStorage, memoryStorage } from './storage.mjs';
23
+ import { supabaseStorage } from './supabase-storage.mjs';
24
+ import { jsonlWal } from './wal.mjs';
25
+ import { llmExtraction, passthroughExtraction } from './extraction.mjs';
26
+ import { openaiChat, openclawChat } from './llm.mjs';
26
27
 
27
28
  /**
28
29
  * Create a configured MemoryGraph instance.
@@ -130,8 +131,8 @@ export function createMemory(opts = {}) {
130
131
  }
131
132
 
132
133
  // LLM (for conflict resolution)
133
- let llm = null;
134
- const llmOpts = opts.llm || {};
134
+ let llm = null;
135
+ const llmOpts = opts.llm || {};
135
136
  if (llmOpts.type === 'openai' && llmOpts.apiKey) {
136
137
  llm = openaiChat({
137
138
  apiKey: llmOpts.apiKey,
@@ -144,16 +145,27 @@ export function createMemory(opts = {}) {
144
145
  port: llmOpts.port,
145
146
  token: llmOpts.token,
146
147
  });
147
- }
148
-
149
- return new MemoryGraph({
150
- storage,
151
- embeddings,
152
- extraction,
153
- llm,
154
- config: {
155
- ...(opts.graph || {}),
156
- ...(opts.predicateSchemas !== undefined ? { predicateSchemas: opts.predicateSchemas } : {}),
148
+ }
149
+
150
+ // Optional local WAL for mutation logging
151
+ let wal = null;
152
+ const walOpts = opts.wal;
153
+ if (walOpts && walOpts.enabled !== false) {
154
+ wal = jsonlWal({
155
+ dir: walOpts.dir,
156
+ filename: walOpts.filename,
157
+ });
158
+ }
159
+
160
+ return new MemoryGraph({
161
+ storage,
162
+ embeddings,
163
+ extraction,
164
+ llm,
165
+ wal,
166
+ config: {
167
+ ...(opts.graph || {}),
168
+ ...(opts.predicateSchemas !== undefined ? { predicateSchemas: opts.predicateSchemas } : {}),
157
169
  },
158
170
  });
159
171
  }
@@ -161,10 +173,34 @@ export function createMemory(opts = {}) {
161
173
  // Re-export everything for advanced usage
162
174
  export { MemoryGraph, tokenize, computeTrust, computeConfidence, estimateTokens, normalizeClaim } from './graph.mjs';
163
175
  export { openaiEmbeddings, noopEmbeddings, cosineSimilarity } from './embeddings.mjs';
164
- export { jsonStorage, memoryStorage } from './storage.mjs';
165
- export { supabaseStorage } from './supabase-storage.mjs';
166
- export { markdownWritethrough, webhookWritethrough } from './writethrough.mjs';
176
+ export { jsonStorage, memoryStorage } from './storage.mjs';
177
+ export { supabaseStorage } from './supabase-storage.mjs';
178
+ export { jsonlWal, createWalMutationEvent, validateWalMutationEvent, WAL_EVENT_VERSION, WAL_MUTATION_OPS } from './wal.mjs';
179
+ export { buildWalReplayTimeline, replayMutationSubset, readAndReplayWal } from './wal-replay.mjs';
180
+ export {
181
+ WAL_SNAPSHOT_VERSION,
182
+ WAL_SNAPSHOT_KIND,
183
+ WAL_SNAPSHOT_FIELDS,
184
+ validateWalSnapshot,
185
+ createWalSnapshot,
186
+ selectWalEventsAfterSnapshot,
187
+ replaySnapshotAndWal,
188
+ } from './wal-snapshot.mjs';
189
+ export {
190
+ projectLiveMutationSubsetState,
191
+ compareMutationSubsetStates,
192
+ compareLiveStateToSnapshotReplay,
193
+ } from './wal-equivalence.mjs';
194
+ export {
195
+ WAL_RECOVERY_MANIFEST_VERSION,
196
+ buildLocalArtifactManifest,
197
+ verifyLocalArtifacts,
198
+ validateRecoveryDryRun,
199
+ formatRecoveryCheckReport,
200
+ } from './wal-recovery.mjs';
201
+ export { markdownWritethrough, webhookWritethrough } from './writethrough.mjs';
167
202
  export { llmExtraction, passthroughExtraction } from './extraction.mjs';
168
203
  export { openaiChat, openclawChat } from './llm.mjs';
169
204
  export { validateBaseUrl } from './validate.mjs';
170
205
  export { detectKeyMoments, heartbeatStore, extractTopicSlug, contextualRecall, preCompactionDump } from './runtime.mjs';
206
+
@@ -490,13 +490,18 @@ export function supabaseStorage({
490
490
  async upsertLinks(sourceId, links) {
491
491
  assertUUID(sourceId, 'upsertLinks: sourceId');
492
492
  if (!links.length) return;
493
- const rows = links.map(l => ({
494
- id: randomUUID(),
495
- source_id: sourceId,
496
- target_id: l.id,
497
- strength: l.similarity,
498
- created_at: new Date().toISOString(),
499
- }));
493
+ // Normalize to canonical direction (sorted pair) to prevent bidirectional dupes.
494
+ // loadLinks() already treats each row as bidirectional, so one row per pair suffices.
495
+ const rows = links.map(l => {
496
+ const [a, b] = [sourceId, l.id].sort();
497
+ return {
498
+ id: randomUUID(),
499
+ source_id: a,
500
+ target_id: b,
501
+ strength: l.similarity,
502
+ created_at: new Date().toISOString(),
503
+ };
504
+ });
500
505
  await request('POST', `/rest/v1/${linksTable}`, rows, {
501
506
  'Prefer': 'return=minimal,resolution=merge-duplicates',
502
507
  });
@@ -0,0 +1,128 @@
1
+ import { normalizeReplayState } from './wal-replay.mjs';
2
+ import { replaySnapshotAndWal } from './wal-snapshot.mjs';
3
+
4
+ function roundTrust(value) {
5
+ return typeof value === 'number' && Number.isFinite(value) ? +value.toFixed(4) : null;
6
+ }
7
+
8
+ function normalizeLiveMemory(memory = {}) {
9
+ const status = typeof memory.status === 'string' && memory.status ? memory.status : 'active';
10
+ const disputes = typeof memory.disputes === 'number' ? Math.max(0, Math.trunc(memory.disputes)) : 0;
11
+ return {
12
+ id: memory.id,
13
+ hasStore: true,
14
+ status,
15
+ category: typeof memory.category === 'string' ? memory.category : null,
16
+ importance:
17
+ typeof memory.importance === 'number' && Number.isFinite(memory.importance)
18
+ ? memory.importance
19
+ : null,
20
+ links: Array.isArray(memory.links) ? memory.links.length : 0,
21
+ reinforcements:
22
+ typeof memory.reinforcements === 'number' && Number.isFinite(memory.reinforcements)
23
+ ? Math.max(0, Math.trunc(memory.reinforcements))
24
+ : 0,
25
+ disputes,
26
+ // Narrow-scope replay only carries trust via dispute events in current WAL subset.
27
+ trust: disputes > 0 ? roundTrust(memory?.provenance?.trust) : null,
28
+ accessCount:
29
+ typeof memory.accessCount === 'number' && Number.isFinite(memory.accessCount)
30
+ ? Math.max(0, Math.trunc(memory.accessCount))
31
+ : 0,
32
+ lastActor: typeof memory.agent === 'string' ? memory.agent : null,
33
+ lastAt:
34
+ typeof memory.updated_at === 'string'
35
+ ? memory.updated_at
36
+ : (typeof memory.created_at === 'string' ? memory.created_at : null),
37
+ quarantine: status === 'quarantined'
38
+ ? {
39
+ reason: memory?.quarantine?.reason ?? null,
40
+ details: memory?.quarantine?.details ?? null,
41
+ }
42
+ : null,
43
+ };
44
+ }
45
+
46
+ export function projectLiveMutationSubsetState(memories = []) {
47
+ if (!Array.isArray(memories)) {
48
+ throw new Error('memories must be an array');
49
+ }
50
+ const byMemoryId = {};
51
+ const ordered = [...memories]
52
+ .filter((memory) => memory && typeof memory.id === 'string' && memory.id)
53
+ .sort((a, b) => a.id.localeCompare(b.id));
54
+ for (const memory of ordered) {
55
+ byMemoryId[memory.id] = normalizeLiveMemory(memory);
56
+ }
57
+ return normalizeReplayState({ byMemoryId, applied: 0 });
58
+ }
59
+
60
+ function comparableRecord(record = {}, { includeTemporal = false } = {}) {
61
+ return {
62
+ id: record.id,
63
+ hasStore: record.hasStore,
64
+ status: record.status,
65
+ category: record.category,
66
+ importance: record.importance,
67
+ links: record.links,
68
+ reinforcements: record.reinforcements,
69
+ disputes: record.disputes,
70
+ trust: record.trust,
71
+ accessCount: record.accessCount,
72
+ lastActor: record.lastActor,
73
+ ...(includeTemporal ? { lastAt: record.lastAt } : {}),
74
+ quarantine: record.quarantine
75
+ ? { reason: record.quarantine.reason ?? null, details: record.quarantine.details ?? null }
76
+ : null,
77
+ };
78
+ }
79
+
80
+ export function compareMutationSubsetStates(liveState, rebuiltState, { includeTemporal = false } = {}) {
81
+ const live = normalizeReplayState(liveState);
82
+ const rebuilt = normalizeReplayState(rebuiltState);
83
+
84
+ const memoryIds = [...new Set([
85
+ ...Object.keys(live.byMemoryId || {}),
86
+ ...Object.keys(rebuilt.byMemoryId || {}),
87
+ ])].sort();
88
+
89
+ const differences = [];
90
+ for (const memoryId of memoryIds) {
91
+ const left = comparableRecord(live.byMemoryId[memoryId] || {}, { includeTemporal });
92
+ const right = comparableRecord(rebuilt.byMemoryId[memoryId] || {}, { includeTemporal });
93
+ const fields = Object.keys({ ...left, ...right });
94
+ for (const field of fields) {
95
+ const l = left[field];
96
+ const r = right[field];
97
+ if (JSON.stringify(l) !== JSON.stringify(r)) {
98
+ differences.push({ memoryId, field, live: l, rebuilt: r });
99
+ }
100
+ }
101
+ }
102
+
103
+ return {
104
+ equivalent: differences.length === 0,
105
+ compared: memoryIds.length,
106
+ differences,
107
+ };
108
+ }
109
+
110
+ export function compareLiveStateToSnapshotReplay({
111
+ liveMemories = [],
112
+ snapshot,
113
+ walEvents = [],
114
+ malformed = [],
115
+ includeTemporal = false,
116
+ } = {}) {
117
+ const liveState = projectLiveMutationSubsetState(liveMemories);
118
+ const rebuilt = replaySnapshotAndWal(snapshot, walEvents, { malformed });
119
+ const rebuiltState = normalizeReplayState(rebuilt);
120
+ const comparison = compareMutationSubsetStates(liveState, rebuiltState, { includeTemporal });
121
+
122
+ return {
123
+ liveState,
124
+ rebuiltState,
125
+ comparison,
126
+ replay: rebuilt,
127
+ };
128
+ }