agentic-flow 2.0.1-alpha.19 → 2.0.1-alpha.20
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/CHANGELOG.md +29 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/intelligence/EmbeddingCache.d.ts +36 -29
- package/dist/intelligence/EmbeddingCache.d.ts.map +1 -1
- package/dist/intelligence/EmbeddingCache.js +461 -90
- package/dist/intelligence/EmbeddingCache.js.map +1 -1
- package/package.json +5 -2
- package/wasm/reasoningbank/reasoningbank_wasm_bg.js +2 -2
- package/wasm/reasoningbank/reasoningbank_wasm_bg.wasm +0 -0
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EmbeddingCache - Persistent
|
|
2
|
+
* EmbeddingCache - Persistent cache for embeddings
|
|
3
3
|
*
|
|
4
4
|
* Makes ONNX embeddings practical by caching across sessions:
|
|
5
5
|
* - First embed: ~400ms (ONNX inference)
|
|
6
|
-
* - Cached embed: ~0.1ms (SQLite lookup)
|
|
6
|
+
* - Cached embed: ~0.1ms (SQLite lookup) or ~0.01ms (in-memory fallback)
|
|
7
7
|
*
|
|
8
|
-
* Storage: ~/.agentic-flow/embedding-cache.db
|
|
8
|
+
* Storage: ~/.agentic-flow/embedding-cache.db (if SQLite available)
|
|
9
|
+
*
|
|
10
|
+
* Windows Compatibility:
|
|
11
|
+
* - Falls back to in-memory cache if better-sqlite3 compilation fails
|
|
12
|
+
* - No native module compilation required for basic functionality
|
|
9
13
|
*/
|
|
10
|
-
import
|
|
11
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
14
|
+
import { existsSync, mkdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
12
15
|
import { join } from 'path';
|
|
13
16
|
import { homedir } from 'os';
|
|
14
17
|
import { createHash } from 'crypto';
|
|
@@ -18,8 +21,303 @@ const DEFAULT_CONFIG = {
|
|
|
18
21
|
maxAgeDays: 30,
|
|
19
22
|
dbPath: join(homedir(), '.agentic-flow', 'embedding-cache.db'),
|
|
20
23
|
dimension: 384,
|
|
24
|
+
forceMemory: false,
|
|
21
25
|
};
|
|
22
|
-
|
|
26
|
+
// Check if better-sqlite3 is available (native, fastest)
|
|
27
|
+
let BetterSqlite3 = null;
|
|
28
|
+
let nativeSqliteAvailable = false;
|
|
29
|
+
// Check if sql.js is available (WASM, cross-platform)
|
|
30
|
+
let SqlJs = null;
|
|
31
|
+
let wasmSqliteAvailable = false;
|
|
32
|
+
try {
|
|
33
|
+
// Try native SQLite first (fastest)
|
|
34
|
+
BetterSqlite3 = require('better-sqlite3');
|
|
35
|
+
nativeSqliteAvailable = true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Native not available, try WASM fallback
|
|
39
|
+
try {
|
|
40
|
+
SqlJs = require('sql.js');
|
|
41
|
+
wasmSqliteAvailable = true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Neither available, will use memory cache
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const sqliteAvailable = nativeSqliteAvailable || wasmSqliteAvailable;
|
|
48
|
+
/**
|
|
49
|
+
* In-memory cache fallback for Windows compatibility
|
|
50
|
+
*/
|
|
51
|
+
class MemoryCache {
|
|
52
|
+
cache = new Map();
|
|
53
|
+
maxEntries;
|
|
54
|
+
hits = 0;
|
|
55
|
+
misses = 0;
|
|
56
|
+
constructor(maxEntries = 10000) {
|
|
57
|
+
this.maxEntries = maxEntries;
|
|
58
|
+
}
|
|
59
|
+
get(hash) {
|
|
60
|
+
const entry = this.cache.get(hash);
|
|
61
|
+
if (entry) {
|
|
62
|
+
entry.hits++;
|
|
63
|
+
entry.accessed = Date.now();
|
|
64
|
+
this.hits++;
|
|
65
|
+
return { embedding: entry.embedding, dimension: entry.embedding.length };
|
|
66
|
+
}
|
|
67
|
+
this.misses++;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
set(hash, text, embedding, model) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
this.cache.set(hash, {
|
|
73
|
+
embedding,
|
|
74
|
+
model,
|
|
75
|
+
hits: 1,
|
|
76
|
+
created: now,
|
|
77
|
+
accessed: now,
|
|
78
|
+
});
|
|
79
|
+
// Evict if over limit
|
|
80
|
+
if (this.cache.size > this.maxEntries) {
|
|
81
|
+
this.evictLRU(Math.ceil(this.maxEntries * 0.1));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
has(hash) {
|
|
85
|
+
return this.cache.has(hash);
|
|
86
|
+
}
|
|
87
|
+
evictLRU(count) {
|
|
88
|
+
const entries = Array.from(this.cache.entries())
|
|
89
|
+
.sort((a, b) => a[1].accessed - b[1].accessed)
|
|
90
|
+
.slice(0, count);
|
|
91
|
+
for (const [key] of entries) {
|
|
92
|
+
this.cache.delete(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
clear() {
|
|
96
|
+
this.cache.clear();
|
|
97
|
+
this.hits = 0;
|
|
98
|
+
this.misses = 0;
|
|
99
|
+
}
|
|
100
|
+
getStats() {
|
|
101
|
+
const entries = Array.from(this.cache.values());
|
|
102
|
+
const oldest = entries.length > 0 ? Math.min(...entries.map(e => e.created)) : 0;
|
|
103
|
+
const newest = entries.length > 0 ? Math.max(...entries.map(e => e.created)) : 0;
|
|
104
|
+
return {
|
|
105
|
+
totalEntries: this.cache.size,
|
|
106
|
+
hits: this.hits,
|
|
107
|
+
misses: this.misses,
|
|
108
|
+
hitRate: this.hits + this.misses > 0 ? this.hits / (this.hits + this.misses) : 0,
|
|
109
|
+
dbSizeBytes: this.cache.size * 384 * 4, // Approximate
|
|
110
|
+
oldestEntry: oldest,
|
|
111
|
+
newestEntry: newest,
|
|
112
|
+
backend: 'memory',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* WASM SQLite cache (sql.js) - Cross-platform with persistence
|
|
118
|
+
* Works on Windows without native compilation
|
|
119
|
+
*/
|
|
120
|
+
class WasmSqliteCache {
|
|
121
|
+
db = null;
|
|
122
|
+
config;
|
|
123
|
+
hits = 0;
|
|
124
|
+
misses = 0;
|
|
125
|
+
dirty = false;
|
|
126
|
+
saveTimeout = null;
|
|
127
|
+
constructor(config) {
|
|
128
|
+
this.config = config;
|
|
129
|
+
}
|
|
130
|
+
async init() {
|
|
131
|
+
if (this.db)
|
|
132
|
+
return;
|
|
133
|
+
// Ensure directory exists
|
|
134
|
+
const dir = join(homedir(), '.agentic-flow');
|
|
135
|
+
if (!existsSync(dir)) {
|
|
136
|
+
mkdirSync(dir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
// Initialize sql.js
|
|
139
|
+
const SQL = await SqlJs();
|
|
140
|
+
// Load existing database or create new
|
|
141
|
+
const dbPath = this.config.dbPath.replace('.db', '-wasm.db');
|
|
142
|
+
try {
|
|
143
|
+
if (existsSync(dbPath)) {
|
|
144
|
+
const buffer = readFileSync(dbPath);
|
|
145
|
+
this.db = new SQL.Database(buffer);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.db = new SQL.Database();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
this.db = new SQL.Database();
|
|
153
|
+
}
|
|
154
|
+
this.initSchema();
|
|
155
|
+
this.cleanupOldEntries();
|
|
156
|
+
}
|
|
157
|
+
initSchema() {
|
|
158
|
+
this.db.run(`
|
|
159
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
160
|
+
hash TEXT PRIMARY KEY,
|
|
161
|
+
text TEXT NOT NULL,
|
|
162
|
+
embedding BLOB NOT NULL,
|
|
163
|
+
dimension INTEGER NOT NULL,
|
|
164
|
+
model TEXT NOT NULL,
|
|
165
|
+
hits INTEGER DEFAULT 1,
|
|
166
|
+
created_at INTEGER NOT NULL,
|
|
167
|
+
last_accessed INTEGER NOT NULL
|
|
168
|
+
)
|
|
169
|
+
`);
|
|
170
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_last_accessed ON embeddings(last_accessed)`);
|
|
171
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_created_at ON embeddings(created_at)`);
|
|
172
|
+
}
|
|
173
|
+
save() {
|
|
174
|
+
// Debounce saves
|
|
175
|
+
if (this.saveTimeout)
|
|
176
|
+
return;
|
|
177
|
+
this.saveTimeout = setTimeout(() => {
|
|
178
|
+
try {
|
|
179
|
+
const data = this.db.export();
|
|
180
|
+
const buffer = Buffer.from(data);
|
|
181
|
+
const dbPath = this.config.dbPath.replace('.db', '-wasm.db');
|
|
182
|
+
writeFileSync(dbPath, buffer);
|
|
183
|
+
this.dirty = false;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
console.warn('[WasmSqliteCache] Save failed:', err);
|
|
187
|
+
}
|
|
188
|
+
this.saveTimeout = null;
|
|
189
|
+
}, 1000);
|
|
190
|
+
}
|
|
191
|
+
get(hash) {
|
|
192
|
+
if (!this.db)
|
|
193
|
+
return null;
|
|
194
|
+
const stmt = this.db.prepare(`SELECT embedding, dimension FROM embeddings WHERE hash = ?`);
|
|
195
|
+
stmt.bind([hash]);
|
|
196
|
+
if (stmt.step()) {
|
|
197
|
+
const row = stmt.getAsObject();
|
|
198
|
+
stmt.free();
|
|
199
|
+
this.hits++;
|
|
200
|
+
this.db.run(`UPDATE embeddings SET hits = hits + 1, last_accessed = ? WHERE hash = ?`, [Date.now(), hash]);
|
|
201
|
+
this.dirty = true;
|
|
202
|
+
this.save();
|
|
203
|
+
// Convert Uint8Array to Float32Array
|
|
204
|
+
const uint8 = row.embedding;
|
|
205
|
+
const float32 = new Float32Array(uint8.buffer, uint8.byteOffset, row.dimension);
|
|
206
|
+
return { embedding: float32, dimension: row.dimension };
|
|
207
|
+
}
|
|
208
|
+
stmt.free();
|
|
209
|
+
this.misses++;
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
set(hash, text, embedding, model) {
|
|
213
|
+
if (!this.db)
|
|
214
|
+
return;
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const buffer = new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
217
|
+
this.db.run(`INSERT OR REPLACE INTO embeddings (hash, text, embedding, dimension, model, hits, created_at, last_accessed)
|
|
218
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`, [hash, text, buffer, embedding.length, model, now, now]);
|
|
219
|
+
this.dirty = true;
|
|
220
|
+
this.maybeEvict();
|
|
221
|
+
this.save();
|
|
222
|
+
}
|
|
223
|
+
has(hash) {
|
|
224
|
+
if (!this.db)
|
|
225
|
+
return false;
|
|
226
|
+
const stmt = this.db.prepare(`SELECT 1 FROM embeddings WHERE hash = ? LIMIT 1`);
|
|
227
|
+
stmt.bind([hash]);
|
|
228
|
+
const found = stmt.step();
|
|
229
|
+
stmt.free();
|
|
230
|
+
return found;
|
|
231
|
+
}
|
|
232
|
+
maybeEvict() {
|
|
233
|
+
const countStmt = this.db.prepare(`SELECT COUNT(*) as count FROM embeddings`);
|
|
234
|
+
countStmt.step();
|
|
235
|
+
const count = countStmt.getAsObject().count;
|
|
236
|
+
countStmt.free();
|
|
237
|
+
if (count > this.config.maxEntries) {
|
|
238
|
+
const toEvict = Math.ceil(this.config.maxEntries * 0.1);
|
|
239
|
+
this.db.run(`DELETE FROM embeddings WHERE hash IN (
|
|
240
|
+
SELECT hash FROM embeddings ORDER BY last_accessed ASC LIMIT ?
|
|
241
|
+
)`, [toEvict]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
cleanupOldEntries() {
|
|
245
|
+
const cutoff = Date.now() - (this.config.maxAgeDays * 24 * 60 * 60 * 1000);
|
|
246
|
+
this.db.run(`DELETE FROM embeddings WHERE created_at < ?`, [cutoff]);
|
|
247
|
+
}
|
|
248
|
+
clear() {
|
|
249
|
+
if (this.db) {
|
|
250
|
+
this.db.run('DELETE FROM embeddings');
|
|
251
|
+
this.hits = 0;
|
|
252
|
+
this.misses = 0;
|
|
253
|
+
this.save();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
close() {
|
|
257
|
+
if (this.saveTimeout) {
|
|
258
|
+
clearTimeout(this.saveTimeout);
|
|
259
|
+
// Force save
|
|
260
|
+
try {
|
|
261
|
+
const data = this.db.export();
|
|
262
|
+
const buffer = Buffer.from(data);
|
|
263
|
+
const dbPath = this.config.dbPath.replace('.db', '-wasm.db');
|
|
264
|
+
writeFileSync(dbPath, buffer);
|
|
265
|
+
}
|
|
266
|
+
catch { }
|
|
267
|
+
}
|
|
268
|
+
if (this.db) {
|
|
269
|
+
this.db.close();
|
|
270
|
+
this.db = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
getStats() {
|
|
274
|
+
if (!this.db) {
|
|
275
|
+
return {
|
|
276
|
+
totalEntries: 0,
|
|
277
|
+
hits: this.hits,
|
|
278
|
+
misses: this.misses,
|
|
279
|
+
hitRate: 0,
|
|
280
|
+
dbSizeBytes: 0,
|
|
281
|
+
oldestEntry: 0,
|
|
282
|
+
newestEntry: 0,
|
|
283
|
+
backend: 'memory',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const countStmt = this.db.prepare(`SELECT COUNT(*) as count FROM embeddings`);
|
|
287
|
+
countStmt.step();
|
|
288
|
+
const count = countStmt.getAsObject().count;
|
|
289
|
+
countStmt.free();
|
|
290
|
+
const oldestStmt = this.db.prepare(`SELECT MIN(created_at) as oldest FROM embeddings`);
|
|
291
|
+
oldestStmt.step();
|
|
292
|
+
const oldest = oldestStmt.getAsObject().oldest || 0;
|
|
293
|
+
oldestStmt.free();
|
|
294
|
+
const newestStmt = this.db.prepare(`SELECT MAX(created_at) as newest FROM embeddings`);
|
|
295
|
+
newestStmt.step();
|
|
296
|
+
const newest = newestStmt.getAsObject().newest || 0;
|
|
297
|
+
newestStmt.free();
|
|
298
|
+
let dbSizeBytes = 0;
|
|
299
|
+
try {
|
|
300
|
+
const dbPath = this.config.dbPath.replace('.db', '-wasm.db');
|
|
301
|
+
const stats = statSync(dbPath);
|
|
302
|
+
dbSizeBytes = stats.size;
|
|
303
|
+
}
|
|
304
|
+
catch { }
|
|
305
|
+
return {
|
|
306
|
+
totalEntries: count,
|
|
307
|
+
hits: this.hits,
|
|
308
|
+
misses: this.misses,
|
|
309
|
+
hitRate: this.hits + this.misses > 0 ? this.hits / (this.hits + this.misses) : 0,
|
|
310
|
+
dbSizeBytes,
|
|
311
|
+
oldestEntry: oldest,
|
|
312
|
+
newestEntry: newest,
|
|
313
|
+
backend: 'file',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Native SQLite cache (better-sqlite3) - Fastest option
|
|
319
|
+
*/
|
|
320
|
+
class SqliteCache {
|
|
23
321
|
db;
|
|
24
322
|
config;
|
|
25
323
|
hits = 0;
|
|
@@ -31,15 +329,16 @@ export class EmbeddingCache {
|
|
|
31
329
|
stmtCount;
|
|
32
330
|
stmtEvictOld;
|
|
33
331
|
stmtEvictLRU;
|
|
34
|
-
|
|
35
|
-
|
|
332
|
+
stmtHas;
|
|
333
|
+
constructor(config) {
|
|
334
|
+
this.config = config;
|
|
36
335
|
// Ensure directory exists
|
|
37
336
|
const dir = join(homedir(), '.agentic-flow');
|
|
38
337
|
if (!existsSync(dir)) {
|
|
39
338
|
mkdirSync(dir, { recursive: true });
|
|
40
339
|
}
|
|
41
340
|
// Open database with WAL mode for better concurrency
|
|
42
|
-
this.db = new
|
|
341
|
+
this.db = new BetterSqlite3(this.config.dbPath);
|
|
43
342
|
this.db.pragma('journal_mode = WAL');
|
|
44
343
|
this.db.pragma('synchronous = NORMAL');
|
|
45
344
|
this.db.pragma('cache_size = 10000');
|
|
@@ -85,6 +384,117 @@ export class EmbeddingCache {
|
|
|
85
384
|
SELECT hash FROM embeddings ORDER BY last_accessed ASC LIMIT ?
|
|
86
385
|
)
|
|
87
386
|
`);
|
|
387
|
+
this.stmtHas = this.db.prepare(`SELECT 1 FROM embeddings WHERE hash = ? LIMIT 1`);
|
|
388
|
+
}
|
|
389
|
+
get(hash) {
|
|
390
|
+
const row = this.stmtGet.get(hash);
|
|
391
|
+
if (row) {
|
|
392
|
+
this.hits++;
|
|
393
|
+
this.stmtUpdateHits.run(Date.now(), hash);
|
|
394
|
+
return {
|
|
395
|
+
embedding: new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.dimension),
|
|
396
|
+
dimension: row.dimension,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
this.misses++;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
set(hash, text, embedding, model) {
|
|
403
|
+
const now = Date.now();
|
|
404
|
+
const buffer = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
405
|
+
this.stmtInsert.run(hash, text, buffer, embedding.length, model, now, now);
|
|
406
|
+
this.maybeEvict();
|
|
407
|
+
}
|
|
408
|
+
has(hash) {
|
|
409
|
+
return this.stmtHas.get(hash) !== undefined;
|
|
410
|
+
}
|
|
411
|
+
maybeEvict() {
|
|
412
|
+
const count = this.stmtCount.get().count;
|
|
413
|
+
if (count > this.config.maxEntries) {
|
|
414
|
+
const toEvict = Math.ceil(this.config.maxEntries * 0.1);
|
|
415
|
+
this.stmtEvictLRU.run(toEvict);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
cleanupOldEntries() {
|
|
419
|
+
const cutoff = Date.now() - (this.config.maxAgeDays * 24 * 60 * 60 * 1000);
|
|
420
|
+
this.stmtEvictOld.run(cutoff);
|
|
421
|
+
}
|
|
422
|
+
clear() {
|
|
423
|
+
this.db.exec('DELETE FROM embeddings');
|
|
424
|
+
this.hits = 0;
|
|
425
|
+
this.misses = 0;
|
|
426
|
+
}
|
|
427
|
+
vacuum() {
|
|
428
|
+
this.db.exec('VACUUM');
|
|
429
|
+
}
|
|
430
|
+
close() {
|
|
431
|
+
this.db.close();
|
|
432
|
+
}
|
|
433
|
+
getStats() {
|
|
434
|
+
const count = this.stmtCount.get().count;
|
|
435
|
+
const oldest = this.db.prepare(`SELECT MIN(created_at) as oldest FROM embeddings`).get();
|
|
436
|
+
const newest = this.db.prepare(`SELECT MAX(created_at) as newest FROM embeddings`).get();
|
|
437
|
+
let dbSizeBytes = 0;
|
|
438
|
+
try {
|
|
439
|
+
const stats = statSync(this.config.dbPath);
|
|
440
|
+
dbSizeBytes = stats.size;
|
|
441
|
+
}
|
|
442
|
+
catch { }
|
|
443
|
+
return {
|
|
444
|
+
totalEntries: count,
|
|
445
|
+
hits: this.hits,
|
|
446
|
+
misses: this.misses,
|
|
447
|
+
hitRate: this.hits + this.misses > 0 ? this.hits / (this.hits + this.misses) : 0,
|
|
448
|
+
dbSizeBytes,
|
|
449
|
+
oldestEntry: oldest.oldest || 0,
|
|
450
|
+
newestEntry: newest.newest || 0,
|
|
451
|
+
backend: 'sqlite',
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* EmbeddingCache - Auto-selects best available backend
|
|
457
|
+
*
|
|
458
|
+
* Backend priority:
|
|
459
|
+
* 1. Native SQLite (better-sqlite3) - Fastest, 9000x speedup
|
|
460
|
+
* 2. WASM SQLite (sql.js) - Cross-platform with persistence
|
|
461
|
+
* 3. Memory cache - Fallback, no persistence
|
|
462
|
+
*/
|
|
463
|
+
export class EmbeddingCache {
|
|
464
|
+
backend;
|
|
465
|
+
config;
|
|
466
|
+
wasmInitPromise = null;
|
|
467
|
+
constructor(config = {}) {
|
|
468
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
469
|
+
// Try native SQLite first (fastest)
|
|
470
|
+
if (nativeSqliteAvailable && !this.config.forceMemory) {
|
|
471
|
+
try {
|
|
472
|
+
this.backend = new SqliteCache(this.config);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
console.warn('[EmbeddingCache] Native SQLite failed, trying WASM fallback');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Try WASM SQLite second (cross-platform with persistence)
|
|
480
|
+
if (wasmSqliteAvailable && !this.config.forceMemory) {
|
|
481
|
+
this.backend = new WasmSqliteCache(this.config);
|
|
482
|
+
this.wasmInitPromise = this.backend.init().catch(err => {
|
|
483
|
+
console.warn('[EmbeddingCache] WASM SQLite init failed, using memory cache');
|
|
484
|
+
this.backend = new MemoryCache(this.config.maxEntries);
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Fallback to memory cache
|
|
489
|
+
this.backend = new MemoryCache(this.config.maxEntries);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Ensure WASM backend is initialized (if using)
|
|
493
|
+
*/
|
|
494
|
+
async ensureInit() {
|
|
495
|
+
if (this.wasmInitPromise) {
|
|
496
|
+
await this.wasmInitPromise;
|
|
497
|
+
}
|
|
88
498
|
}
|
|
89
499
|
/**
|
|
90
500
|
* Generate hash key for text + model combination
|
|
@@ -94,44 +504,33 @@ export class EmbeddingCache {
|
|
|
94
504
|
}
|
|
95
505
|
/**
|
|
96
506
|
* Get embedding from cache
|
|
97
|
-
* Returns null if not found
|
|
98
507
|
*/
|
|
99
508
|
get(text, model = 'default') {
|
|
100
509
|
const hash = this.hashKey(text, model);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
this.hits++;
|
|
104
|
-
// Update access time and hit count
|
|
105
|
-
this.stmtUpdateHits.run(Date.now(), hash);
|
|
106
|
-
// Convert Buffer back to Float32Array
|
|
107
|
-
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.dimension);
|
|
108
|
-
}
|
|
109
|
-
this.misses++;
|
|
110
|
-
return null;
|
|
510
|
+
const result = this.backend.get(hash);
|
|
511
|
+
return result ? result.embedding : null;
|
|
111
512
|
}
|
|
112
513
|
/**
|
|
113
514
|
* Store embedding in cache
|
|
114
515
|
*/
|
|
115
516
|
set(text, embedding, model = 'default') {
|
|
116
517
|
const hash = this.hashKey(text, model);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
518
|
+
if (this.backend instanceof SqliteCache) {
|
|
519
|
+
this.backend.set(hash, text, embedding, model);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
this.backend.set(hash, text, embedding, model);
|
|
523
|
+
}
|
|
123
524
|
}
|
|
124
525
|
/**
|
|
125
526
|
* Check if text is cached
|
|
126
527
|
*/
|
|
127
528
|
has(text, model = 'default') {
|
|
128
529
|
const hash = this.hashKey(text, model);
|
|
129
|
-
|
|
130
|
-
return row !== undefined;
|
|
530
|
+
return this.backend.has(hash);
|
|
131
531
|
}
|
|
132
532
|
/**
|
|
133
533
|
* Get multiple embeddings at once
|
|
134
|
-
* Returns map of text -> embedding (only cached ones)
|
|
135
534
|
*/
|
|
136
535
|
getMany(texts, model = 'default') {
|
|
137
536
|
const result = new Map();
|
|
@@ -147,87 +546,53 @@ export class EmbeddingCache {
|
|
|
147
546
|
* Store multiple embeddings at once
|
|
148
547
|
*/
|
|
149
548
|
setMany(entries, model = 'default') {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
for (const { text, embedding } of items) {
|
|
153
|
-
const hash = this.hashKey(text, model);
|
|
154
|
-
const buffer = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
155
|
-
this.stmtInsert.run(hash, text, buffer, embedding.length, model, now, now);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
insertMany(entries);
|
|
159
|
-
this.maybeEvict();
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Evict old or LRU entries if over limit
|
|
163
|
-
*/
|
|
164
|
-
maybeEvict() {
|
|
165
|
-
const count = this.stmtCount.get().count;
|
|
166
|
-
if (count > this.config.maxEntries) {
|
|
167
|
-
// Evict 10% of entries (LRU)
|
|
168
|
-
const toEvict = Math.ceil(this.config.maxEntries * 0.1);
|
|
169
|
-
this.stmtEvictLRU.run(toEvict);
|
|
549
|
+
for (const { text, embedding } of entries) {
|
|
550
|
+
this.set(text, embedding, model);
|
|
170
551
|
}
|
|
171
552
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Cleanup entries older than maxAgeDays
|
|
174
|
-
*/
|
|
175
|
-
cleanupOldEntries() {
|
|
176
|
-
const cutoff = Date.now() - (this.config.maxAgeDays * 24 * 60 * 60 * 1000);
|
|
177
|
-
this.stmtEvictOld.run(cutoff);
|
|
178
|
-
}
|
|
179
553
|
/**
|
|
180
554
|
* Get cache statistics
|
|
181
555
|
*/
|
|
182
556
|
getStats() {
|
|
183
|
-
|
|
184
|
-
const oldest = this.db.prepare(`SELECT MIN(created_at) as oldest FROM embeddings`).get();
|
|
185
|
-
const newest = this.db.prepare(`SELECT MAX(created_at) as newest FROM embeddings`).get();
|
|
186
|
-
// Get database file size
|
|
187
|
-
let dbSizeBytes = 0;
|
|
188
|
-
try {
|
|
189
|
-
const fs = require('fs');
|
|
190
|
-
const stats = fs.statSync(this.config.dbPath);
|
|
191
|
-
dbSizeBytes = stats.size;
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
// Ignore
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
totalEntries: count,
|
|
198
|
-
hits: this.hits,
|
|
199
|
-
misses: this.misses,
|
|
200
|
-
hitRate: this.hits + this.misses > 0 ? this.hits / (this.hits + this.misses) : 0,
|
|
201
|
-
dbSizeBytes,
|
|
202
|
-
oldestEntry: oldest.oldest || 0,
|
|
203
|
-
newestEntry: newest.newest || 0,
|
|
204
|
-
};
|
|
557
|
+
return this.backend.getStats();
|
|
205
558
|
}
|
|
206
559
|
/**
|
|
207
560
|
* Clear all cached embeddings
|
|
208
561
|
*/
|
|
209
562
|
clear() {
|
|
210
|
-
this.
|
|
211
|
-
this.hits = 0;
|
|
212
|
-
this.misses = 0;
|
|
563
|
+
this.backend.clear();
|
|
213
564
|
}
|
|
214
565
|
/**
|
|
215
|
-
*
|
|
216
|
-
*/
|
|
217
|
-
clearModel(model) {
|
|
218
|
-
this.db.prepare('DELETE FROM embeddings WHERE model = ?').run(model);
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Vacuum database to reclaim space
|
|
566
|
+
* Vacuum database (SQLite only)
|
|
222
567
|
*/
|
|
223
568
|
vacuum() {
|
|
224
|
-
this.
|
|
569
|
+
if (this.backend instanceof SqliteCache) {
|
|
570
|
+
this.backend.vacuum();
|
|
571
|
+
}
|
|
225
572
|
}
|
|
226
573
|
/**
|
|
227
574
|
* Close database connection
|
|
228
575
|
*/
|
|
229
576
|
close() {
|
|
230
|
-
this.
|
|
577
|
+
if (this.backend instanceof SqliteCache || this.backend instanceof WasmSqliteCache) {
|
|
578
|
+
this.backend.close();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Check if using SQLite backend (native or WASM)
|
|
583
|
+
*/
|
|
584
|
+
isSqliteBackend() {
|
|
585
|
+
return this.backend instanceof SqliteCache || this.backend instanceof WasmSqliteCache;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get backend type
|
|
589
|
+
*/
|
|
590
|
+
getBackendType() {
|
|
591
|
+
if (this.backend instanceof SqliteCache)
|
|
592
|
+
return 'native';
|
|
593
|
+
if (this.backend instanceof WasmSqliteCache)
|
|
594
|
+
return 'wasm';
|
|
595
|
+
return 'memory';
|
|
231
596
|
}
|
|
232
597
|
}
|
|
233
598
|
// Singleton instance
|
|
@@ -250,4 +615,10 @@ export function resetEmbeddingCache() {
|
|
|
250
615
|
cacheInstance = null;
|
|
251
616
|
}
|
|
252
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Check if SQLite is available
|
|
620
|
+
*/
|
|
621
|
+
export function isSqliteAvailable() {
|
|
622
|
+
return sqliteAvailable;
|
|
623
|
+
}
|
|
253
624
|
//# sourceMappingURL=EmbeddingCache.js.map
|