sqlite-local-ai-memory 0.0.0-stage → 1.4.4

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.
@@ -0,0 +1,409 @@
1
+ /**
2
+ * ============================================================================
3
+ * FILE: src/memory/sqlite_store.ts
4
+ * RESPONSIBILITY: Bare-Metal In-Process SQLite Persistence Engine.
5
+ * INTEGRATION: FTS5 BM25 exact matching + Int8 Quantized Vector Cosine (<1.5ms).
6
+ *
7
+ * JOURNAL MODE (SQLITE_JOURNAL_MODE env var, default: TRUNCATE):
8
+ * - TRUNCATE (default): safe for network/NFS/cloud-mounted disks where WAL
9
+ * shared-memory files (-shm/-wal) can cause corruption. mmap is disabled.
10
+ * - WAL: best for local SSD/NVMe — activates 256MB mmap and concurrent reads.
11
+ * Use only when the database file lives on a local filesystem.
12
+ * ============================================================================
13
+ */
14
+ import Database from "better-sqlite3";
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import os from "os";
18
+ import { EMBEDDING_DIM } from "./types.js";
19
+ const LOCAL_FALLBACK_PATH = path.join(os.homedir(), ".local-ai-memory", "memoria.db");
20
+ export const DASHBOARD_UUID = "00000000-0000-0000-0000-000000000000";
21
+ export class SqliteStore {
22
+ db;
23
+ dbPath;
24
+ constructor(targetPath) {
25
+ const rawPath = targetPath || process.env.SQLITE_MEM_PATH;
26
+ this.dbPath = this.resolveDatabasePath(rawPath);
27
+ this.db = this.initDatabase(this.dbPath);
28
+ }
29
+ expandHome(filepath) {
30
+ const trimmed = filepath.trim();
31
+ if (trimmed === "~")
32
+ return os.homedir();
33
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
34
+ return path.join(os.homedir(), trimmed.slice(2));
35
+ }
36
+ return trimmed;
37
+ }
38
+ resolveDatabasePath(customPath) {
39
+ if (!customPath)
40
+ return LOCAL_FALLBACK_PATH;
41
+ const expandedPath = this.expandHome(customPath);
42
+ try {
43
+ const dir = path.dirname(expandedPath);
44
+ if (!fs.existsSync(dir))
45
+ fs.mkdirSync(dir, { recursive: true });
46
+ return expandedPath;
47
+ }
48
+ catch (err) {
49
+ console.error(`⚠️ [SQLITE_STORE] Database path "${expandedPath}" unusable (${err.message}). ` +
50
+ `Falling back to: ${LOCAL_FALLBACK_PATH}`);
51
+ return LOCAL_FALLBACK_PATH;
52
+ }
53
+ }
54
+ initDatabase(dbPath) {
55
+ const dir = path.dirname(dbPath);
56
+ if (!fs.existsSync(dir))
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ const journalMode = (process.env.SQLITE_JOURNAL_MODE || "TRUNCATE").toUpperCase();
59
+ const busyTimeout = parseInt(process.env.SQLITE_BUSY_TIMEOUT || "5000", 10);
60
+ const db = new Database(dbPath, { timeout: busyTimeout });
61
+ db.pragma(`busy_timeout = ${busyTimeout}`);
62
+ try {
63
+ // FIX R3-1.2: better-sqlite3 returns an Array of row objects, not a plain object.
64
+ const rows = db.pragma("journal_mode");
65
+ const currentMode = rows?.[0]?.journal_mode?.toUpperCase() || "";
66
+ if (currentMode !== journalMode) {
67
+ db.pragma(`journal_mode = ${journalMode}`);
68
+ }
69
+ }
70
+ catch (err) {
71
+ console.error("⚠️ [SQLITE_STORE] Could not verify/set journal_mode:", err.message);
72
+ }
73
+ db.pragma("synchronous = NORMAL");
74
+ if (journalMode === "WAL") {
75
+ db.pragma("mmap_size = 268435456");
76
+ }
77
+ else {
78
+ db.pragma("mmap_size = 0");
79
+ if (journalMode !== "TRUNCATE") {
80
+ // TRUNCATE is the intentional default — only warn for unexpected modes.
81
+ console.error(`⚠️ [SQLITE_STORE] journal_mode=${journalMode} + synchronous=NORMAL: ` +
82
+ `mmap disabled. For local SSD/NVMe use SQLITE_JOURNAL_MODE=WAL.`);
83
+ }
84
+ }
85
+ db.exec(`
86
+ CREATE VIRTUAL TABLE IF NOT EXISTS recuerdos_fts USING fts5(
87
+ point_id UNINDEXED,
88
+ user_id UNINDEXED,
89
+ data,
90
+ source,
91
+ created_at UNINDEXED,
92
+ tokenize = 'unicode61 remove_diacritics 1'
93
+ );
94
+
95
+ CREATE TABLE IF NOT EXISTS recuerdos_vectores (
96
+ point_id TEXT NOT NULL,
97
+ user_id TEXT NOT NULL,
98
+ data TEXT NOT NULL,
99
+ source TEXT DEFAULT 'system',
100
+ created_at TEXT NOT NULL,
101
+ vector_blob BLOB,
102
+ PRIMARY KEY (point_id, user_id)
103
+ );
104
+
105
+ CREATE INDEX IF NOT EXISTS idx_recuerdos_user_id ON recuerdos_vectores (user_id);
106
+ CREATE INDEX IF NOT EXISTS idx_recuerdos_source ON recuerdos_vectores (source);
107
+ CREATE INDEX IF NOT EXISTS idx_recuerdos_created_at ON recuerdos_vectores (created_at);
108
+ `);
109
+ return db;
110
+ }
111
+ quantizeVectorInt8(vec) {
112
+ let norm = 0;
113
+ for (let i = 0; i < vec.length; i++)
114
+ norm += vec[i] * vec[i];
115
+ norm = Math.sqrt(norm);
116
+ const scale = norm > 0 ? 1.0 / norm : 1.0;
117
+ const buf = Buffer.alloc(vec.length);
118
+ for (let i = 0; i < vec.length; i++) {
119
+ const normalized = vec[i] * scale;
120
+ const val = Math.max(-127, Math.min(127, Math.round(normalized * 127.0)));
121
+ buf.writeInt8(val, i);
122
+ }
123
+ return buf;
124
+ }
125
+ cosineSimilarityInt8(queryVec, blob) {
126
+ if (!blob || blob.length !== queryVec.length)
127
+ return 0;
128
+ let dotProduct = 0;
129
+ let normQ = 0;
130
+ let normB = 0;
131
+ for (let i = 0; i < queryVec.length; i++) {
132
+ const q = queryVec[i];
133
+ const b = blob.readInt8(i) / 127.0;
134
+ dotProduct += q * b;
135
+ normQ += q * q;
136
+ normB += b * b;
137
+ }
138
+ if (normQ === 0 || normB === 0)
139
+ return 0;
140
+ return dotProduct / (Math.sqrt(normQ) * Math.sqrt(normB));
141
+ }
142
+ /** Static stopword set for ES/EN — filters common words that dilute FTS5 BM25 ranking. */
143
+ static STOPWORDS = new Set([
144
+ // Spanish
145
+ "de", "la", "el", "en", "que", "es", "lo", "un", "una", "las", "los",
146
+ "del", "al", "por", "con", "para", "se", "su", "sus", "le", "les",
147
+ "ya", "me", "te", "mi", "tu", "nos", "como", "pero", "mas", "ese",
148
+ "esa", "eso", "esta", "este", "esto", "son", "fue", "ser", "hay",
149
+ "si", "no", "muy", "sin", "sobre", "entre", "cuando", "donde",
150
+ "desde", "hasta", "era", "han", "sido", "tiene", "puede",
151
+ "tambien", "otro", "otra", "otros", "cada", "todo", "toda", "todos",
152
+ // English
153
+ "the", "is", "at", "which", "on", "a", "an", "and", "or", "but",
154
+ "in", "with", "for", "to", "of", "it", "its", "was", "are", "be",
155
+ "has", "had", "have", "do", "does", "did", "not", "this", "that",
156
+ "from", "by", "as", "into", "can", "will", "just", "about", "also",
157
+ "been", "would", "should", "could", "than", "then", "them", "they",
158
+ "their", "there", "what", "when", "how", "who", "all", "some",
159
+ ]);
160
+ searchFTS5(query, limit = 10, userId) {
161
+ try {
162
+ const sanitized = query.replace(/[^a-zA-Z0-9\u00C0-\u024F\s]/g, " ").trim();
163
+ const cleanWords = sanitized
164
+ .split(/\s+/)
165
+ .map((w) => w.trim())
166
+ .filter((w) => w.length > 1 && !SqliteStore.STOPWORDS.has(w.toLowerCase()));
167
+ if (cleanWords.length === 0)
168
+ return [];
169
+ const ftsQuery = cleanWords.map((w) => `"${w}"*`).join(" OR ");
170
+ let rows;
171
+ if (userId) {
172
+ const stmt = this.db.prepare(`
173
+ SELECT point_id as id, user_id, data, source, created_at, rank
174
+ FROM recuerdos_fts
175
+ WHERE recuerdos_fts MATCH ? AND user_id = ?
176
+ ORDER BY rank LIMIT ?
177
+ `);
178
+ rows = stmt.all(ftsQuery, userId, limit);
179
+ }
180
+ else {
181
+ const stmt = this.db.prepare(`
182
+ SELECT point_id as id, user_id, data, source, created_at, rank
183
+ FROM recuerdos_fts
184
+ WHERE recuerdos_fts MATCH ?
185
+ ORDER BY rank LIMIT ?
186
+ `);
187
+ rows = stmt.all(ftsQuery, limit);
188
+ }
189
+ return rows.map((r) => ({
190
+ id: String(r.id),
191
+ user_id: String(r.user_id),
192
+ data: String(r.data),
193
+ memory: String(r.data),
194
+ source: String(r.source),
195
+ created_at: String(r.created_at),
196
+ rank: typeof r.rank === "number" ? r.rank : undefined,
197
+ }));
198
+ }
199
+ catch (error) {
200
+ console.error("[SQLITE_STORE] Error in FTS5 search:", error.message);
201
+ return [];
202
+ }
203
+ }
204
+ searchVectorInt8(queryVec, limit = 10, userId) {
205
+ try {
206
+ let rows;
207
+ if (userId) {
208
+ const stmt = this.db.prepare(`
209
+ SELECT point_id as id, user_id, data, source, created_at, vector_blob
210
+ FROM recuerdos_vectores
211
+ WHERE vector_blob IS NOT NULL
212
+ AND point_id != ?
213
+ AND source != 'inactive'
214
+ AND user_id = ?
215
+ ORDER BY created_at DESC
216
+ LIMIT 5000
217
+ `);
218
+ rows = stmt.all(DASHBOARD_UUID, userId);
219
+ if (rows.length >= 5000) {
220
+ console.error(`⚠️ [SQLITE_STORE] Vector scan hit the 5000-row safety limit for user [${userId}]. Consider enabling ANN indexing for better scalability.`);
221
+ }
222
+ }
223
+ else {
224
+ const stmt = this.db.prepare(`
225
+ SELECT point_id as id, user_id, data, source, created_at, vector_blob
226
+ FROM recuerdos_vectores
227
+ WHERE vector_blob IS NOT NULL
228
+ AND point_id != ?
229
+ AND source != 'inactive'
230
+ ORDER BY created_at DESC
231
+ LIMIT 5000
232
+ `);
233
+ rows = stmt.all(DASHBOARD_UUID);
234
+ if (rows.length >= 5000) {
235
+ console.error(`⚠️ [SQLITE_STORE] Vector scan hit the 5000-row safety limit. Consider enabling ANN indexing for better scalability.`);
236
+ }
237
+ }
238
+ const scored = [];
239
+ for (const r of rows) {
240
+ if (r.vector_blob && Buffer.isBuffer(r.vector_blob)) {
241
+ const score = this.cosineSimilarityInt8(queryVec, r.vector_blob);
242
+ scored.push({
243
+ id: String(r.id),
244
+ user_id: String(r.user_id),
245
+ data: String(r.data),
246
+ memory: String(r.data),
247
+ source: String(r.source),
248
+ created_at: String(r.created_at),
249
+ score,
250
+ });
251
+ }
252
+ }
253
+ scored.sort((a, b) => (b.score || 0) - (a.score || 0));
254
+ return scored.slice(0, limit);
255
+ }
256
+ catch (error) {
257
+ console.error("[SQLITE_STORE] Error in Int8 vector search:", error.message);
258
+ return [];
259
+ }
260
+ }
261
+ insertMemoryFact(id, data, userId = "user_default", vector, source = "system") {
262
+ try {
263
+ const now = new Date().toISOString();
264
+ const vectorBlob = vector && vector.length === EMBEDDING_DIM ? this.quantizeVectorInt8(vector) : null;
265
+ const targetUser = userId || "user_default";
266
+ const deleteFts = this.db.prepare(`DELETE FROM recuerdos_fts WHERE point_id = ?`);
267
+ const insertFts = this.db.prepare(`
268
+ INSERT INTO recuerdos_fts (point_id, user_id, data, source, created_at)
269
+ VALUES (?, ?, ?, ?, ?)
270
+ `);
271
+ const insertVec = this.db.prepare(`
272
+ INSERT OR REPLACE INTO recuerdos_vectores (point_id, user_id, data, source, created_at, vector_blob)
273
+ VALUES (?, ?, ?, ?, ?, ?)
274
+ `);
275
+ this.db.transaction(() => {
276
+ deleteFts.run(id);
277
+ insertFts.run(id, targetUser, data.trim(), source, now);
278
+ insertVec.run(id, targetUser, data.trim(), source, now, vectorBlob);
279
+ })();
280
+ return true;
281
+ }
282
+ catch (error) {
283
+ console.error("[SQLITE_STORE] Error inserting fact:", error.message);
284
+ return false;
285
+ }
286
+ }
287
+ deactivateMemoryFact(id) {
288
+ if (id === DASHBOARD_UUID)
289
+ return false;
290
+ try {
291
+ const deleteFts = this.db.prepare("DELETE FROM recuerdos_fts WHERE point_id = ?");
292
+ const updateVec = this.db.prepare("UPDATE recuerdos_vectores SET vector_blob = NULL, source = 'inactive' WHERE point_id = ?");
293
+ this.db.transaction(() => {
294
+ deleteFts.run(id);
295
+ updateVec.run(id);
296
+ })();
297
+ return true;
298
+ }
299
+ catch (error) {
300
+ console.error("[SQLITE_STORE] Error deactivating fact:", error.message);
301
+ return false;
302
+ }
303
+ }
304
+ getDashboardFact(userId = "user_default") {
305
+ try {
306
+ const row = this.db.prepare(`
307
+ SELECT data, created_at as updated_at
308
+ FROM recuerdos_vectores
309
+ WHERE point_id = ? AND user_id = ?
310
+ `).get(DASHBOARD_UUID, userId);
311
+ if (row && row.data) {
312
+ return {
313
+ data: String(row.data),
314
+ memory: String(row.data),
315
+ updated_at: String(row.updated_at || "")
316
+ };
317
+ }
318
+ }
319
+ catch (error) {
320
+ console.error("[SQLITE_STORE] Error reading Dashboard:", error.message);
321
+ }
322
+ return null;
323
+ }
324
+ saveDashboardFact(dashboardJson, userId = "user_default", vector) {
325
+ try {
326
+ const now = new Date().toISOString();
327
+ const vectorBlob = vector && vector.length === EMBEDDING_DIM ? this.quantizeVectorInt8(vector) : null;
328
+ this.db.prepare(`
329
+ INSERT OR REPLACE INTO recuerdos_vectores (point_id, user_id, data, source, created_at, vector_blob)
330
+ VALUES (?, ?, ?, 'lo_ultimo', ?, ?)
331
+ `).run(DASHBOARD_UUID, userId, dashboardJson, now, vectorBlob);
332
+ return true;
333
+ }
334
+ catch (error) {
335
+ console.error("[SQLITE_STORE] Error saving Dashboard:", error.message);
336
+ return false;
337
+ }
338
+ }
339
+ /**
340
+ * Retrieves recent memory facts created on or after the specified timestamp.
341
+ * Excludes the Dashboard global singleton record (DASHBOARD_UUID).
342
+ *
343
+ * @param sinceIso - ISO 8601 timestamp string marking the start of the consolidation window.
344
+ * @param userId - Target user identifier (defaults to 'user_default').
345
+ * @param sourceFilter - Optional case-insensitive source label filter (e.g. 'TOTALCONNECT').
346
+ * @returns Array of objects containing the fact text (`data`) and its origin (`source`).
347
+ */
348
+ getRecentFactsSince(sinceIso, userId = "user_default", sourceFilter) {
349
+ try {
350
+ let query = `
351
+ SELECT data, source
352
+ FROM recuerdos_vectores
353
+ WHERE user_id = ? AND point_id != ?
354
+ AND created_at >= ?
355
+ AND source != 'inactive'
356
+ `;
357
+ const params = [userId, DASHBOARD_UUID, sinceIso];
358
+ if (sourceFilter) {
359
+ query += ` AND UPPER(source) = ?`;
360
+ params.push(sourceFilter.toUpperCase());
361
+ }
362
+ query += ` ORDER BY created_at ASC`;
363
+ const rows = this.db.prepare(query).all(...params);
364
+ return rows.map((r) => ({
365
+ data: String(r.data),
366
+ source: String(r.source || "system"),
367
+ }));
368
+ }
369
+ catch (error) {
370
+ console.error("[SQLITE_STORE] Error retrieving recent facts:", error.message);
371
+ return [];
372
+ }
373
+ }
374
+ async backfillMissingVectors(getEmbeddingFn, userId = "user_default") {
375
+ try {
376
+ const rows = this.db.prepare(`
377
+ SELECT point_id, data
378
+ FROM recuerdos_vectores
379
+ WHERE user_id = ? AND vector_blob IS NULL AND source != 'inactive' AND point_id != ?
380
+ LIMIT 25
381
+ `).all(userId, DASHBOARD_UUID);
382
+ if (rows.length === 0)
383
+ return 0;
384
+ let backfilled = 0;
385
+ for (const r of rows) {
386
+ const vec = await getEmbeddingFn(String(r.data));
387
+ if (vec && vec.length === EMBEDDING_DIM) {
388
+ const blob = this.quantizeVectorInt8(vec);
389
+ this.db.prepare(`
390
+ UPDATE recuerdos_vectores SET vector_blob = ? WHERE point_id = ?
391
+ `).run(blob, String(r.point_id));
392
+ backfilled++;
393
+ }
394
+ }
395
+ if (backfilled > 0) {
396
+ console.error(`✨ [LAZY_BACKFILL] Generated Int8 vector embeddings for ${backfilled} past un-vectorized facts.`);
397
+ }
398
+ return backfilled;
399
+ }
400
+ catch (e) {
401
+ return 0;
402
+ }
403
+ }
404
+ close() {
405
+ if (this.db) {
406
+ this.db.close();
407
+ }
408
+ }
409
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stress_test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stress_test.d.ts","sourceRoot":"","sources":["../src/stress_test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * ============================================================================
3
+ * FILE: src/memory/stress_test.ts
4
+ * RESPONSIBILITY: Comprehensive 26-Test Stress Suite (Real-World Read/Write,
5
+ * Noise Filtering, Semantic Collision Resolution, and AutoDream Consolidation).
6
+ * ============================================================================
7
+ */
8
+ import fs from "fs";
9
+ import { MemoryEngine } from "./MemoryEngine.js";
10
+ async function runStressTest() {
11
+ console.log("🚀 [STRESS_TEST] Starting 26-Test Intensive Live Suite on MemoryEngine...\n");
12
+ const testDbPath = "./memoria_stress.db";
13
+ for (const ext of ["", "-wal", "-shm"]) {
14
+ if (fs.existsSync(testDbPath + ext)) {
15
+ try {
16
+ fs.unlinkSync(testDbPath + ext);
17
+ }
18
+ catch (_) { }
19
+ }
20
+ }
21
+ const memory = new MemoryEngine({
22
+ dbPath: testDbPath,
23
+ userName: "User Alice",
24
+ });
25
+ let totalTests = 0;
26
+ let successfulTests = 0;
27
+ function assertTest(name, condition, details) {
28
+ totalTests++;
29
+ if (condition) {
30
+ successfulTests++;
31
+ console.log(`✅ [TEST ${totalTests}] ${name} ${details ? `(${details})` : ""}`);
32
+ }
33
+ else {
34
+ console.error(`❌ [TEST ${totalTests}] FAILED: ${name} ${details ? `(${details})` : ""}`);
35
+ }
36
+ }
37
+ try {
38
+ // ------------------------------------------------------------------------
39
+ // BLOCK 1: Noise & Greeting Filtering (Gatekeeper) - 5 Tests
40
+ // ------------------------------------------------------------------------
41
+ console.log("--- BLOCK 1: Noise & Greeting Filtering (Agent: Gatekeeper) ---");
42
+ await memory.save("hello good afternoon");
43
+ assertTest("Regex Filter: Hello good afternoon", (await memory.search("hello")).length === 0);
44
+ await memory.save("ok thanks got it");
45
+ assertTest("Regex Filter: ok thanks got it", (await memory.search("thanks")).length === 0);
46
+ await memory.save("hahahahaha sounds good cool");
47
+ assertTest("Regex Filter: laughter and empty confirmation", (await memory.search("sounds")).length === 0);
48
+ await memory.save("sure sounds good to me");
49
+ assertTest("Regex Filter: sure sounds good to me", (await memory.search("sure")).length === 0);
50
+ await memory.save("got it all set");
51
+ assertTest("Regex Filter: got it all set", (await memory.search("all set")).length === 0);
52
+ // ------------------------------------------------------------------------
53
+ // BLOCK 2: Atomic Fact Extraction (Notary) - 5 Tests
54
+ // ------------------------------------------------------------------------
55
+ console.log("\n--- BLOCK 2: Atomic Fact Extraction (Agent: Notary) ---");
56
+ await memory.save("Primary server Proxmox is active on IP 10.0.0.200");
57
+ const h1 = await memory.search("Proxmox IP 10.0.0.200");
58
+ assertTest("Notary: Extract Proxmox IP .200", h1.length > 0 && h1[0].data.includes("10.0.0.200"));
59
+ await memory.save("User Alice likes to drink double espresso coffee in the morning");
60
+ const h2 = await memory.search("espresso coffee");
61
+ assertTest("Notary: Coffee preference", h2.length > 0 && h2[0].data.includes("espresso"));
62
+ await memory.save("Secondary AdGuard Home container is hosted on IP 10.0.0.96");
63
+ const h3 = await memory.search("AdGuard IP 10.0.0.96");
64
+ assertTest("Notary: AdGuard IP .96", h3.length > 0 && h3[0].data.includes("10.0.0.96"));
65
+ await memory.save("User Bob prefers not to work in noisy environments");
66
+ const h4 = await memory.search("User Bob noisy environments");
67
+ assertTest("Notary: Preference of User Bob", h4.length > 0 && h4[0].data.includes("User Bob"));
68
+ await memory.save("SnapRAID backup script runs daily at 02:00 AM");
69
+ const h5 = await memory.search("SnapRAID 02:00 AM");
70
+ assertTest("Notary: SnapRAID schedule", h5.length > 0 && h5[0].data.includes("SnapRAID"));
71
+ // ------------------------------------------------------------------------
72
+ // BLOCK 3: Additive Facts (Context Coexistence) - 4 Tests
73
+ // ------------------------------------------------------------------------
74
+ console.log("\n--- BLOCK 3: Additive Facts (Context Coexistence) ---");
75
+ await memory.save("User Alice also likes drinking green tea in the afternoon");
76
+ const hAdd1 = await memory.search("espresso coffee or green tea");
77
+ assertTest("Additive Facts: Coffee and Tea Coexist", hAdd1.length >= 2, `Retrieved ${hAdd1.length} facts`);
78
+ await memory.save("Proxmox server also hosts virtual machine VM 101 Tiny11");
79
+ const hAdd2 = await memory.search("Proxmox VM 101 Tiny11");
80
+ assertTest("Additive Facts: Proxmox hosts VM 101", hAdd2.length > 0);
81
+ await memory.save("User Bob prefers studying programming in Python");
82
+ const hAdd3 = await memory.search("User Bob Python");
83
+ assertTest("Additive Facts: User Bob studies Python", hAdd3.length > 0);
84
+ await memory.save("SnapRAID includes parity on backup disk /mnt/parity");
85
+ const hAdd4 = await memory.search("SnapRAID /mnt/parity");
86
+ assertTest("Additive Facts: SnapRAID parity", hAdd4.length > 0);
87
+ // ------------------------------------------------------------------------
88
+ // BLOCK 4: Semantic Arbiter (Soft-Deletes & State Replacement) - 5 Tests
89
+ // ------------------------------------------------------------------------
90
+ console.log("\n--- BLOCK 4: Semantic Arbiter (Soft-Deletes & State Replacement) ---");
91
+ await memory.save("Secondary AdGuard Home container was moved to new IP 10.0.0.97");
92
+ const hCol1 = await memory.search("AdGuard Home IP");
93
+ assertTest("Semantic Arbiter: AdGuard IP updated to .97", hCol1.length > 0 && hCol1[0].data.includes("10.0.0.97"));
94
+ await memory.save("JottaCloud sync service was temporarily paused for maintenance");
95
+ const hCol2 = await memory.search("JottaCloud maintenance");
96
+ assertTest("Semantic Arbiter: JottaCloud paused state", hCol2.length > 0 && hCol2[0].data.includes("paused"));
97
+ await memory.save("JottaCloud sync service was resumed and is 100% operational");
98
+ const hCol3 = await memory.search("JottaCloud operational");
99
+ assertTest("Semantic Arbiter: JottaCloud resumed state", hCol3.length > 0 && hCol3[0].data.includes("operational"));
100
+ await memory.save("User Alice is currently at the office");
101
+ const hCol4 = await memory.search("User Alice location office");
102
+ assertTest("Semantic Arbiter: Alice location at office", hCol4.length > 0 && hCol4[0].data.includes("office"));
103
+ await memory.save("User Alice has returned home");
104
+ const hCol5 = await memory.search("User Alice location home");
105
+ assertTest("Semantic Arbiter: Alice location at home", hCol5.length > 0 && hCol5[0].data.includes("home"));
106
+ // ------------------------------------------------------------------------
107
+ // BLOCK 5: Dual Hybrid Search (FTS5 BM25 + Int8 Vector) - 4 Tests
108
+ // ------------------------------------------------------------------------
109
+ console.log("\n--- BLOCK 5: Dual Hybrid Search (FTS5 BM25 + Int8 Vector) ---");
110
+ const search1 = await memory.search("10.0.0.200", 3);
111
+ assertTest("Exact Dual Search IP: 10.0.0.200", search1.length > 0 && (search1[0].score || 0) >= 0.5, `Score: ${search1[0]?.score?.toFixed(2)}`);
112
+ const search2 = await memory.search("What beverages does Alice prefer?", 3);
113
+ assertTest("Semantic Dual Search: Alice beverages", search2.length > 0, `Facts found: ${search2.length}`);
114
+ const search3 = await memory.search("User Bob programming preferences", 3);
115
+ assertTest("Compound Dual Search: User Bob", search3.length > 0, `Facts found: ${search3.length}`);
116
+ const search4 = await memory.search("SnapRAID parity and schedule", 3);
117
+ assertTest("Compound Dual Search: SnapRAID", search4.length > 0, `Facts found: ${search4.length}`);
118
+ // ------------------------------------------------------------------------
119
+ // BLOCK 6: AutoDream State Orchestrator (Dashboard Consolidation) - 3 Tests
120
+ // ------------------------------------------------------------------------
121
+ console.log("\n--- BLOCK 6: AutoDream State Orchestrator (Dashboard Consolidation) ---");
122
+ const autoDreamResult = await memory.consolidate();
123
+ assertTest("AutoDream: Consolidation Execution", autoDreamResult.statusMessage.includes("AutoDream") || autoDreamResult.narrativeSummary.length > 0, autoDreamResult.statusMessage);
124
+ const dashboardObj = memory.getDashboard();
125
+ assertTest("Dashboard: Read Consolidated State", dashboardObj !== null && dashboardObj.data.length > 0, `Active dashboard: ${dashboardObj?.updated_at}`);
126
+ const searchFinal = await memory.search("server state summary", 3);
127
+ assertTest("Final Search after AutoDream", searchFinal.length > 0);
128
+ // ------------------------------------------------------------------------
129
+ // FINAL SUMMARY
130
+ // ------------------------------------------------------------------------
131
+ console.log("\n==========================================================================");
132
+ console.log(`📊 FINAL STRESS TEST SUMMARY: ${successfulTests} / ${totalTests} TESTS PASSED (${((successfulTests / totalTests) * 100).toFixed(1)}%)`);
133
+ console.log("==========================================================================");
134
+ if (successfulTests < totalTests) {
135
+ console.error(`💥 ${totalTests - successfulTests} test(s) failed.`);
136
+ // FIX R3-1.5: set exitCode instead of calling process.exit() so the
137
+ // finally block below always runs and closes the SQLite connection cleanly.
138
+ process.exitCode = 1;
139
+ }
140
+ }
141
+ catch (error) {
142
+ console.error("❌ Fatal error during test suite:", error.message);
143
+ process.exitCode = 1;
144
+ }
145
+ finally {
146
+ memory.close();
147
+ }
148
+ }
149
+ runStressTest();
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test_autodream_dual.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_autodream_dual.d.ts","sourceRoot":"","sources":["../src/test_autodream_dual.ts"],"names":[],"mappings":""}
@@ -0,0 +1,38 @@
1
+ import { MemoryEngine } from "./MemoryEngine.js";
2
+ import * as fs from "fs";
3
+ async function runTest() {
4
+ console.log("🧪 [TEST] Testing AutoDream Dual Output & Memory Triage...");
5
+ const dbPath = "./memoria_test_dual.db";
6
+ for (const ext of ["", "-wal", "-shm"]) {
7
+ if (fs.existsSync(dbPath + ext)) {
8
+ try {
9
+ fs.unlinkSync(dbPath + ext);
10
+ }
11
+ catch (_) { }
12
+ }
13
+ }
14
+ const memory = new MemoryEngine({
15
+ dbPath,
16
+ userName: "Alice",
17
+ userId: "user_test",
18
+ semanticArbitrator: true,
19
+ });
20
+ console.log("\n📥 Ingesting test facts...");
21
+ await memory.saveFact("[DEVELOPMENT] [NETWORK] Socket reconnection error 256X resolved by adding delay before resumable calls.");
22
+ await memory.saveFact("[DIAGNOSIS] Investigating server overheating root cause under heavy load. Solution pending.");
23
+ console.log("\n⚙️ Executing AutoDream consolidation (memory.consolidate())...");
24
+ const result = await memory.consolidate("user_test");
25
+ console.log("\n📊 DUAL RESULT COLLECTED:");
26
+ console.log("--------------------------------------------------");
27
+ console.log("🔹 Narrative Summary (Dashboard):", result.narrativeSummary);
28
+ console.log("\n🔹 Dashboard Items (Working State):", JSON.stringify(result.dashboard, null, 2));
29
+ console.log("\n🔹 Triage Memory (Long-term SQLite Facts):", JSON.stringify(result.triageMemory, null, 2));
30
+ console.log("\n🔹 Open Cases (Incubator):", JSON.stringify(result.openCases, null, 2));
31
+ console.log("--------------------------------------------------");
32
+ memory.close();
33
+ console.log("\n✅ Test completed successfully.");
34
+ }
35
+ runTest().catch((err) => {
36
+ console.error("❌ Test error:", err);
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test_autodream_history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test_autodream_history.d.ts","sourceRoot":"","sources":["../src/test_autodream_history.ts"],"names":[],"mappings":""}