@mnemoai/core 1.1.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.
Files changed (49) hide show
  1. package/index.ts +3395 -0
  2. package/openclaw.plugin.json +815 -0
  3. package/package.json +59 -0
  4. package/src/access-tracker.ts +341 -0
  5. package/src/adapters/README.md +78 -0
  6. package/src/adapters/chroma.ts +206 -0
  7. package/src/adapters/lancedb.ts +237 -0
  8. package/src/adapters/pgvector.ts +218 -0
  9. package/src/adapters/qdrant.ts +191 -0
  10. package/src/adaptive-retrieval.ts +90 -0
  11. package/src/audit-log.ts +238 -0
  12. package/src/chunker.ts +254 -0
  13. package/src/config.ts +271 -0
  14. package/src/decay-engine.ts +238 -0
  15. package/src/embedder.ts +735 -0
  16. package/src/extraction-prompts.ts +339 -0
  17. package/src/license.ts +258 -0
  18. package/src/llm-client.ts +125 -0
  19. package/src/mcp-server.ts +415 -0
  20. package/src/memory-categories.ts +71 -0
  21. package/src/memory-upgrader.ts +388 -0
  22. package/src/migrate.ts +364 -0
  23. package/src/mnemo.ts +142 -0
  24. package/src/noise-filter.ts +97 -0
  25. package/src/noise-prototypes.ts +164 -0
  26. package/src/observability.ts +81 -0
  27. package/src/query-tracker.ts +57 -0
  28. package/src/reflection-event-store.ts +98 -0
  29. package/src/reflection-item-store.ts +112 -0
  30. package/src/reflection-mapped-metadata.ts +84 -0
  31. package/src/reflection-metadata.ts +23 -0
  32. package/src/reflection-ranking.ts +33 -0
  33. package/src/reflection-retry.ts +181 -0
  34. package/src/reflection-slices.ts +265 -0
  35. package/src/reflection-store.ts +602 -0
  36. package/src/resonance-state.ts +85 -0
  37. package/src/retriever.ts +1510 -0
  38. package/src/scopes.ts +375 -0
  39. package/src/self-improvement-files.ts +143 -0
  40. package/src/semantic-gate.ts +121 -0
  41. package/src/session-recovery.ts +138 -0
  42. package/src/smart-extractor.ts +923 -0
  43. package/src/smart-metadata.ts +561 -0
  44. package/src/storage-adapter.ts +153 -0
  45. package/src/store.ts +1330 -0
  46. package/src/tier-manager.ts +189 -0
  47. package/src/tools.ts +1292 -0
  48. package/src/wal-recovery.ts +172 -0
  49. package/test/core.test.mjs +301 -0
@@ -0,0 +1,172 @@
1
+ // SPDX-License-Identifier: LicenseRef-Mnemo-Pro
2
+ /**
3
+ * Graphiti Write-Ahead Log (WAL) + Recovery
4
+ *
5
+ * Ensures Graphiti writes survive transient failures by logging pending writes
6
+ * to a JSONL file. On plugin startup, pending entries older than 1 hour are
7
+ * retried automatically.
8
+ */
9
+
10
+ import { appendFile, readFile, mkdir } from "node:fs/promises";
11
+ import { existsSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join, dirname } from "node:path";
14
+
15
+ // ============================================================================
16
+ // WAL Path
17
+ // ============================================================================
18
+
19
+ const WAL_PATH = join(homedir(), ".openclaw", "memory", "graphiti-wal.jsonl");
20
+
21
+ // ============================================================================
22
+ // WAL Entry Types
23
+ // ============================================================================
24
+
25
+ export interface WalEntry {
26
+ ts: string;
27
+ action: "write";
28
+ text: string;
29
+ scope: string;
30
+ category: string;
31
+ groupId: string;
32
+ importance: number;
33
+ status: "pending" | "committed" | "failed";
34
+ error?: string;
35
+ }
36
+
37
+ // ============================================================================
38
+ // WAL Writer
39
+ // ============================================================================
40
+
41
+ async function ensureWalDir(): Promise<void> {
42
+ await mkdir(dirname(WAL_PATH), { recursive: true });
43
+ }
44
+
45
+ export async function walAppend(entry: WalEntry): Promise<void> {
46
+ await ensureWalDir();
47
+ const line = JSON.stringify(entry) + "\n";
48
+ await appendFile(WAL_PATH, line, "utf8");
49
+ }
50
+
51
+ export async function walMarkCommitted(ts: string): Promise<void> {
52
+ const entry: WalEntry = {
53
+ ts,
54
+ action: "write",
55
+ text: "",
56
+ scope: "",
57
+ category: "",
58
+ groupId: "",
59
+ importance: 0,
60
+ status: "committed",
61
+ };
62
+ await ensureWalDir();
63
+ await appendFile(WAL_PATH, JSON.stringify(entry) + "\n", "utf8");
64
+ }
65
+
66
+ export async function walMarkFailed(ts: string, error: string): Promise<void> {
67
+ const entry: WalEntry = {
68
+ ts,
69
+ action: "write",
70
+ text: "",
71
+ scope: "",
72
+ category: "",
73
+ groupId: "",
74
+ importance: 0,
75
+ status: "failed",
76
+ error,
77
+ };
78
+ await ensureWalDir();
79
+ await appendFile(WAL_PATH, JSON.stringify(entry) + "\n", "utf8");
80
+ }
81
+
82
+ // ============================================================================
83
+ // WAL Recovery
84
+ // ============================================================================
85
+
86
+ /**
87
+ * Scan the WAL file for entries with status=pending whose ts > 1 hour ago.
88
+ * Retry writing them to Graphiti and mark as committed or failed.
89
+ */
90
+ export async function recoverPendingWrites(): Promise<{ recovered: number; failed: number }> {
91
+ if (!existsSync(WAL_PATH)) {
92
+ return { recovered: 0, failed: 0 };
93
+ }
94
+
95
+ let raw: string;
96
+ try {
97
+ raw = await readFile(WAL_PATH, "utf8");
98
+ } catch {
99
+ return { recovered: 0, failed: 0 };
100
+ }
101
+
102
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
103
+
104
+ // Build a map of latest status per ts
105
+ const statusMap = new Map<string, WalEntry>();
106
+ for (const line of lines) {
107
+ try {
108
+ const entry = JSON.parse(line) as WalEntry;
109
+ // Later entries for the same ts override earlier ones
110
+ const existing = statusMap.get(entry.ts);
111
+ if (!existing || entry.status !== "pending") {
112
+ statusMap.set(entry.ts, entry);
113
+ }
114
+ } catch {
115
+ // Skip malformed lines
116
+ }
117
+ }
118
+
119
+ // Find pending entries older than 1 hour
120
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
121
+ const pending: WalEntry[] = [];
122
+ for (const entry of statusMap.values()) {
123
+ if (entry.status === "pending") {
124
+ const entryTime = new Date(entry.ts).getTime();
125
+ if (entryTime < oneHourAgo) {
126
+ pending.push(entry);
127
+ }
128
+ }
129
+ }
130
+
131
+ if (pending.length === 0) {
132
+ return { recovered: 0, failed: 0 };
133
+ }
134
+
135
+ const graphitiBase = process.env.GRAPHITI_BASE_URL || "http://127.0.0.1:18799";
136
+ let recovered = 0;
137
+ let failed = 0;
138
+
139
+ for (const entry of pending) {
140
+ try {
141
+ const response = await fetch(`${graphitiBase}/episodes`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({
145
+ text: `[${entry.category}] ${entry.text}`,
146
+ group_id: entry.groupId,
147
+ reference_time: entry.ts,
148
+ source: `lancedb-pro-store-${entry.groupId}`,
149
+ category: entry.category,
150
+ }),
151
+ signal: AbortSignal.timeout(15000),
152
+ });
153
+
154
+ if (response.ok) {
155
+ await walMarkCommitted(entry.ts);
156
+ recovered++;
157
+ } else {
158
+ await walMarkFailed(entry.ts, `HTTP ${response.status}`);
159
+ failed++;
160
+ }
161
+ } catch (err) {
162
+ await walMarkFailed(entry.ts, String(err));
163
+ failed++;
164
+ }
165
+ }
166
+
167
+ console.log(
168
+ `mnemo: WAL recovery — recovered=${recovered}, failed=${failed}, total_pending=${pending.length}`,
169
+ );
170
+
171
+ return { recovered, failed };
172
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Mnemo Core — Test Suite
3
+ * Using Node.js built-in test runner (node --test)
4
+ * No external test framework needed.
5
+ */
6
+
7
+ import { describe, it } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { createHash, generateKeyPairSync, sign, verify, createPublicKey, createPrivateKey } from "node:crypto";
10
+ import { hostname, arch, cpus, platform } from "node:os";
11
+
12
+ // ============================================================================
13
+ // Group 1: SQL Injection Prevention
14
+ // ============================================================================
15
+
16
+ describe("escapeSqlLiteral (allowlist sanitizer)", () => {
17
+ // Re-implement the function here for testing (same logic as store.ts)
18
+ function escapeSqlLiteral(value) {
19
+ if (typeof value !== "string") return "";
20
+ return value.replace(/[^a-zA-Z0-9\-_.:@ \u4e00-\u9fff\u3400-\u4dbf]/g, "");
21
+ }
22
+
23
+ it("passes normal ID through", () => {
24
+ assert.equal(escapeSqlLiteral("abc-123-def"), "abc-123-def");
25
+ });
26
+
27
+ it("passes UUID through", () => {
28
+ assert.equal(
29
+ escapeSqlLiteral("550e8400-e29b-41d4-a716-446655440000"),
30
+ "550e8400-e29b-41d4-a716-446655440000"
31
+ );
32
+ });
33
+
34
+ it("passes Chinese text through", () => {
35
+ assert.equal(escapeSqlLiteral("用户偏好"), "用户偏好");
36
+ });
37
+
38
+ it("strips single quotes (SQL injection)", () => {
39
+ assert.equal(escapeSqlLiteral("'; DROP TABLE memories; --"), " DROP TABLE memories --");
40
+ });
41
+
42
+ it("strips parentheses", () => {
43
+ assert.equal(escapeSqlLiteral("id = '1' OR (1=1)"), "id 1 OR 11");
44
+ });
45
+
46
+ it("strips backticks", () => {
47
+ assert.equal(escapeSqlLiteral("`memories`"), "memories");
48
+ });
49
+
50
+ it("strips semicolons", () => {
51
+ assert.equal(escapeSqlLiteral("abc; DELETE FROM x"), "abc DELETE FROM x");
52
+ });
53
+
54
+ it("handles empty string", () => {
55
+ assert.equal(escapeSqlLiteral(""), "");
56
+ });
57
+
58
+ it("handles non-string input", () => {
59
+ assert.equal(escapeSqlLiteral(null), "");
60
+ assert.equal(escapeSqlLiteral(undefined), "");
61
+ assert.equal(escapeSqlLiteral(123), "");
62
+ });
63
+
64
+ it("preserves scope format", () => {
65
+ assert.equal(escapeSqlLiteral("agent:bot3"), "agent:bot3");
66
+ assert.equal(escapeSqlLiteral("global"), "global");
67
+ });
68
+
69
+ it("preserves email-like strings", () => {
70
+ assert.equal(escapeSqlLiteral("user@example.com"), "user@example.com");
71
+ });
72
+ });
73
+
74
+ // ============================================================================
75
+ // Group 2: License Key System
76
+ // ============================================================================
77
+
78
+ describe("License Key (Ed25519)", () => {
79
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
80
+ const pubDer = publicKey.export({ type: "spki", format: "der" });
81
+ const privDer = privateKey.export({ type: "pkcs8", format: "der" });
82
+
83
+ function signPayload(payload) {
84
+ const buf = Buffer.from(JSON.stringify(payload));
85
+ const sig = sign(null, buf, privateKey);
86
+ return buf.toString("base64") + "." + sig.toString("base64");
87
+ }
88
+
89
+ function verifyKey(key) {
90
+ const dot = key.indexOf(".");
91
+ if (dot < 1) return null;
92
+ try {
93
+ const payloadBuf = Buffer.from(key.slice(0, dot), "base64");
94
+ const sigBuf = Buffer.from(key.slice(dot + 1), "base64");
95
+ const valid = verify(null, payloadBuf, publicKey, sigBuf);
96
+ if (!valid) return null;
97
+ return JSON.parse(payloadBuf.toString());
98
+ } catch { return null; }
99
+ }
100
+
101
+ it("valid key verifies correctly", () => {
102
+ const key = signPayload({ licensee: "Test", plan: "indie" });
103
+ const payload = verifyKey(key);
104
+ assert.notEqual(payload, null);
105
+ assert.equal(payload.licensee, "Test");
106
+ });
107
+
108
+ it("tampered payload fails", () => {
109
+ const key = signPayload({ licensee: "Test" });
110
+ const parts = key.split(".");
111
+ const original = JSON.parse(Buffer.from(parts[0], "base64").toString());
112
+ original.licensee = "Hacked";
113
+ const tampered = Buffer.from(JSON.stringify(original)).toString("base64") + "." + parts[1];
114
+ assert.equal(verifyKey(tampered), null);
115
+ });
116
+
117
+ it("tampered signature fails", () => {
118
+ const key = signPayload({ licensee: "Test" });
119
+ const parts = key.split(".");
120
+ const sigBuf = Buffer.from(parts[1], "base64");
121
+ sigBuf[0] ^= 0xff;
122
+ const tampered = parts[0] + "." + sigBuf.toString("base64");
123
+ assert.equal(verifyKey(tampered), null);
124
+ });
125
+
126
+ it("empty string fails", () => {
127
+ assert.equal(verifyKey(""), null);
128
+ });
129
+
130
+ it("wrong key pair fails", () => {
131
+ const { privateKey: otherPriv } = generateKeyPairSync("ed25519");
132
+ const buf = Buffer.from(JSON.stringify({ licensee: "Evil" }));
133
+ const sig = sign(null, buf, otherPriv);
134
+ const key = buf.toString("base64") + "." + sig.toString("base64");
135
+ assert.equal(verifyKey(key), null);
136
+ });
137
+
138
+ it("expired key detected", () => {
139
+ const payload = { licensee: "Test", expires: "2020-01-01" };
140
+ const key = signPayload(payload);
141
+ const decoded = verifyKey(key);
142
+ assert.notEqual(decoded, null);
143
+ assert.ok(new Date(decoded.expires).getTime() < Date.now());
144
+ });
145
+ });
146
+
147
+ // ============================================================================
148
+ // Group 3: Machine Fingerprint
149
+ // ============================================================================
150
+
151
+ describe("Machine Fingerprint", () => {
152
+ function getMachineFingerprint() {
153
+ const cpu = cpus()[0]?.model || "unknown";
154
+ const raw = `${hostname()}:${arch()}:${cpu}:${platform()}`;
155
+ return createHash("sha256").update(raw).digest("hex");
156
+ }
157
+
158
+ it("produces SHA-256 hex (64 chars)", () => {
159
+ const fp = getMachineFingerprint();
160
+ assert.match(fp, /^[a-f0-9]{64}$/);
161
+ });
162
+
163
+ it("is deterministic", () => {
164
+ assert.equal(getMachineFingerprint(), getMachineFingerprint());
165
+ });
166
+ });
167
+
168
+ // ============================================================================
169
+ // Group 4: Storage Adapter Interface
170
+ // ============================================================================
171
+
172
+ describe("StorageAdapter Registry", async () => {
173
+ // Dynamic import to test the module
174
+ let registerAdapter, createAdapter, listAdapters;
175
+ try {
176
+ const mod = await import("../src/storage-adapter.js");
177
+ registerAdapter = mod.registerAdapter;
178
+ createAdapter = mod.createAdapter;
179
+ listAdapters = mod.listAdapters;
180
+ } catch {
181
+ // Skip if module can't be loaded (TS not compiled)
182
+ console.log(" ⚠ Skipping adapter tests (TS modules not compiled)");
183
+ return;
184
+ }
185
+
186
+ it("registerAdapter + createAdapter round-trip", () => {
187
+ registerAdapter("test-backend", () => ({ name: "test-backend" }));
188
+ const adapter = createAdapter("test-backend");
189
+ assert.equal(adapter.name, "test-backend");
190
+ });
191
+
192
+ it("listAdapters includes registered backends", () => {
193
+ const list = listAdapters();
194
+ assert.ok(list.includes("test-backend"));
195
+ });
196
+
197
+ it("createAdapter throws for unknown backend", () => {
198
+ assert.throws(() => createAdapter("nonexistent"), /not found/);
199
+ });
200
+ });
201
+
202
+ // ============================================================================
203
+ // Group 5: Audit Log
204
+ // ============================================================================
205
+
206
+ describe("Audit Log Entry Format", () => {
207
+ it("audit entry has required fields", () => {
208
+ const entry = {
209
+ timestamp: new Date().toISOString(),
210
+ action: "create",
211
+ actor: "agent:default",
212
+ memoryIds: ["mem_001"],
213
+ scope: "global",
214
+ reason: "auto-capture",
215
+ };
216
+
217
+ assert.ok(entry.timestamp);
218
+ assert.ok(["create", "update", "delete", "expire", "recall"].includes(entry.action));
219
+ assert.ok(Array.isArray(entry.memoryIds));
220
+ assert.ok(entry.actor);
221
+ });
222
+
223
+ it("audit entry serializes to valid JSON", () => {
224
+ const entry = {
225
+ timestamp: "2026-03-23T10:00:00Z",
226
+ action: "update",
227
+ actor: "system",
228
+ memoryIds: ["mem_001", "mem_002"],
229
+ reason: "contradiction",
230
+ details: JSON.stringify({ old: "age 30", new: "age 31" }),
231
+ };
232
+ const json = JSON.stringify(entry);
233
+ const parsed = JSON.parse(json);
234
+ assert.deepEqual(parsed, entry);
235
+ });
236
+ });
237
+
238
+ // ============================================================================
239
+ // Group 6: Weibull Decay Model
240
+ // ============================================================================
241
+
242
+ describe("Weibull Decay", () => {
243
+ function weibull(t, beta, halflife) {
244
+ const lam = halflife / Math.pow(Math.LN2, 1 / beta);
245
+ return Math.exp(-Math.pow(t / lam, beta));
246
+ }
247
+
248
+ it("score is 1.0 at t=0", () => {
249
+ assert.equal(weibull(0, 1.0, 30), 1.0);
250
+ });
251
+
252
+ it("score is ~0.5 at t=halflife for beta=1.0", () => {
253
+ const score = weibull(30, 1.0, 30);
254
+ assert.ok(Math.abs(score - 0.5) < 0.01);
255
+ });
256
+
257
+ it("Core (beta=0.8) retains more than Working after 2x halflife", () => {
258
+ const core = weibull(60, 0.8, 30);
259
+ const working = weibull(60, 1.0, 30);
260
+ assert.ok(core > working, `Core ${core} should be > Working ${working} at t=2*halflife`);
261
+ });
262
+
263
+ it("Peripheral (beta=1.3) retains less than Working after 2x halflife", () => {
264
+ const peripheral = weibull(60, 1.3, 30);
265
+ const working = weibull(60, 1.0, 30);
266
+ assert.ok(peripheral < working, `Peripheral ${peripheral} should be < Working ${working} at t=2*halflife`);
267
+ });
268
+
269
+ it("score approaches 0 for very old memories", () => {
270
+ const score = weibull(365, 1.0, 30);
271
+ assert.ok(score < 0.001);
272
+ });
273
+
274
+ it("Core memories retain >50% at 90 days", () => {
275
+ const score = weibull(90, 0.8, 90);
276
+ assert.ok(score > 0.45);
277
+ });
278
+ });
279
+
280
+ // ============================================================================
281
+ // Group 7: Config Path Resolution
282
+ // ============================================================================
283
+
284
+ describe("Config Path Defaults", () => {
285
+ it("MNEMO_DB_PATH env overrides default", () => {
286
+ const original = process.env.MNEMO_DB_PATH;
287
+ process.env.MNEMO_DB_PATH = "/custom/path";
288
+ // The function checks env first
289
+ assert.equal(process.env.MNEMO_DB_PATH, "/custom/path");
290
+ if (original) process.env.MNEMO_DB_PATH = original;
291
+ else delete process.env.MNEMO_DB_PATH;
292
+ });
293
+
294
+ it("MNEMO_CONFIG env is respected", () => {
295
+ const original = process.env.MNEMO_CONFIG;
296
+ process.env.MNEMO_CONFIG = "/custom/config.json";
297
+ assert.equal(process.env.MNEMO_CONFIG, "/custom/config.json");
298
+ if (original) process.env.MNEMO_CONFIG = original;
299
+ else delete process.env.MNEMO_CONFIG;
300
+ });
301
+ });