@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 +43 -6
- package/package.json +14 -5
- package/src/graph.mjs +86 -32
- package/src/index.mjs +55 -19
- package/src/supabase-storage.mjs +12 -7
- package/src/wal-equivalence.mjs +128 -0
- package/src/wal-recovery.mjs +492 -0
- package/src/wal-replay.mjs +191 -0
- package/src/wal-snapshot.mjs +191 -0
- package/src/wal.mjs +111 -0
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.
|
|
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
|
|
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
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
25
|
-
import {
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 {
|
|
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
|
+
|
package/src/supabase-storage.mjs
CHANGED
|
@@ -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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
}
|