@aitytech/agentkits-memory 1.0.0 → 2.0.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 (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -149
  3. package/assets/agentkits-memory-add-memory.png +0 -0
  4. package/assets/agentkits-memory-memory-detail.png +0 -0
  5. package/assets/agentkits-memory-memory-list.png +0 -0
  6. package/assets/logo.svg +24 -0
  7. package/dist/better-sqlite3-backend.d.ts +192 -0
  8. package/dist/better-sqlite3-backend.d.ts.map +1 -0
  9. package/dist/better-sqlite3-backend.js +801 -0
  10. package/dist/better-sqlite3-backend.js.map +1 -0
  11. package/dist/cli/save.js +0 -0
  12. package/dist/cli/setup.d.ts +6 -2
  13. package/dist/cli/setup.d.ts.map +1 -1
  14. package/dist/cli/setup.js +289 -42
  15. package/dist/cli/setup.js.map +1 -1
  16. package/dist/cli/viewer.js +25 -56
  17. package/dist/cli/viewer.js.map +1 -1
  18. package/dist/cli/web-viewer.d.ts +14 -0
  19. package/dist/cli/web-viewer.d.ts.map +1 -0
  20. package/dist/cli/web-viewer.js +1769 -0
  21. package/dist/cli/web-viewer.js.map +1 -0
  22. package/dist/embeddings/embedding-cache.d.ts +131 -0
  23. package/dist/embeddings/embedding-cache.d.ts.map +1 -0
  24. package/dist/embeddings/embedding-cache.js +217 -0
  25. package/dist/embeddings/embedding-cache.js.map +1 -0
  26. package/dist/embeddings/index.d.ts +11 -0
  27. package/dist/embeddings/index.d.ts.map +1 -0
  28. package/dist/embeddings/index.js +11 -0
  29. package/dist/embeddings/index.js.map +1 -0
  30. package/dist/embeddings/local-embeddings.d.ts +140 -0
  31. package/dist/embeddings/local-embeddings.d.ts.map +1 -0
  32. package/dist/embeddings/local-embeddings.js +293 -0
  33. package/dist/embeddings/local-embeddings.js.map +1 -0
  34. package/dist/hooks/context.d.ts +6 -1
  35. package/dist/hooks/context.d.ts.map +1 -1
  36. package/dist/hooks/context.js +12 -2
  37. package/dist/hooks/context.js.map +1 -1
  38. package/dist/hooks/observation.d.ts +6 -1
  39. package/dist/hooks/observation.d.ts.map +1 -1
  40. package/dist/hooks/observation.js +12 -2
  41. package/dist/hooks/observation.js.map +1 -1
  42. package/dist/hooks/service.d.ts +1 -6
  43. package/dist/hooks/service.d.ts.map +1 -1
  44. package/dist/hooks/service.js +33 -85
  45. package/dist/hooks/service.js.map +1 -1
  46. package/dist/hooks/session-init.d.ts +6 -1
  47. package/dist/hooks/session-init.d.ts.map +1 -1
  48. package/dist/hooks/session-init.js +12 -2
  49. package/dist/hooks/session-init.js.map +1 -1
  50. package/dist/hooks/summarize.d.ts +6 -1
  51. package/dist/hooks/summarize.d.ts.map +1 -1
  52. package/dist/hooks/summarize.js +12 -2
  53. package/dist/hooks/summarize.js.map +1 -1
  54. package/dist/index.d.ts +10 -17
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +172 -94
  57. package/dist/index.js.map +1 -1
  58. package/dist/mcp/server.js +17 -3
  59. package/dist/mcp/server.js.map +1 -1
  60. package/dist/migration.js +3 -3
  61. package/dist/migration.js.map +1 -1
  62. package/dist/search/hybrid-search.d.ts +262 -0
  63. package/dist/search/hybrid-search.d.ts.map +1 -0
  64. package/dist/search/hybrid-search.js +688 -0
  65. package/dist/search/hybrid-search.js.map +1 -0
  66. package/dist/search/index.d.ts +13 -0
  67. package/dist/search/index.d.ts.map +1 -0
  68. package/dist/search/index.js +13 -0
  69. package/dist/search/index.js.map +1 -0
  70. package/dist/search/token-economics.d.ts +161 -0
  71. package/dist/search/token-economics.d.ts.map +1 -0
  72. package/dist/search/token-economics.js +239 -0
  73. package/dist/search/token-economics.js.map +1 -0
  74. package/dist/types.d.ts +0 -68
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js.map +1 -1
  77. package/package.json +23 -8
  78. package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
  79. package/src/__tests__/cache-manager.test.ts +499 -0
  80. package/src/__tests__/embedding-integration.test.ts +481 -0
  81. package/src/__tests__/hnsw-index.test.ts +727 -0
  82. package/src/__tests__/index.test.ts +432 -0
  83. package/src/better-sqlite3-backend.ts +1000 -0
  84. package/src/cli/setup.ts +358 -47
  85. package/src/cli/viewer.ts +28 -63
  86. package/src/cli/web-viewer.ts +1956 -0
  87. package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
  88. package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
  89. package/src/embeddings/embedding-cache.ts +318 -0
  90. package/src/embeddings/index.ts +20 -0
  91. package/src/embeddings/local-embeddings.ts +419 -0
  92. package/src/hooks/__tests__/handlers.test.ts +58 -17
  93. package/src/hooks/__tests__/integration.test.ts +77 -26
  94. package/src/hooks/context.ts +13 -2
  95. package/src/hooks/observation.ts +13 -2
  96. package/src/hooks/service.ts +39 -100
  97. package/src/hooks/session-init.ts +13 -2
  98. package/src/hooks/summarize.ts +13 -2
  99. package/src/index.ts +210 -116
  100. package/src/mcp/server.ts +20 -3
  101. package/src/search/__tests__/hybrid-search.test.ts +669 -0
  102. package/src/search/__tests__/token-economics.test.ts +276 -0
  103. package/src/search/hybrid-search.ts +968 -0
  104. package/src/search/index.ts +29 -0
  105. package/src/search/token-economics.ts +367 -0
  106. package/src/types.ts +0 -96
  107. package/src/__tests__/sqljs-backend.test.ts +0 -410
  108. package/src/migration.ts +0 -574
  109. package/src/sql.js.d.ts +0 -70
  110. package/src/sqljs-backend.ts +0 -789
@@ -0,0 +1,801 @@
1
+ /**
2
+ * BetterSqlite3Backend - Native SQLite with FTS5 Trigram for CJK Support
3
+ *
4
+ * Production-grade backend using better-sqlite3 (native SQLite).
5
+ * Provides:
6
+ * - FTS5 with trigram tokenizer for CJK (Japanese, Chinese, Korean)
7
+ * - BM25 ranking for relevance scoring
8
+ * - 10x faster than sql.js for large datasets
9
+ * - Proper word segmentation for all languages
10
+ *
11
+ * Requires:
12
+ * - Node.js environment (no browser support)
13
+ * - npm install better-sqlite3
14
+ *
15
+ * @module @agentkits/memory/better-sqlite3-backend
16
+ */
17
+ import { EventEmitter } from 'node:events';
18
+ import { existsSync, mkdirSync } from 'node:fs';
19
+ import * as path from 'node:path';
20
+ /**
21
+ * Default configuration values
22
+ */
23
+ const DEFAULT_CONFIG = {
24
+ databasePath: ':memory:',
25
+ optimize: true,
26
+ defaultNamespace: 'default',
27
+ maxEntries: 1000000,
28
+ verbose: false,
29
+ ftsTokenizer: 'trigram', // Best for CJK out of the box
30
+ };
31
+ /**
32
+ * Estimate tokens from content (~4 chars per token)
33
+ */
34
+ function estimateTokens(text) {
35
+ return Math.ceil(text.length / 4);
36
+ }
37
+ /**
38
+ * BetterSqlite3 Backend for Production Memory Storage
39
+ *
40
+ * Features:
41
+ * - Native SQLite performance (10x faster than sql.js)
42
+ * - FTS5 with trigram tokenizer for CJK language support
43
+ * - BM25 relevance ranking
44
+ * - WAL mode for concurrent reads
45
+ * - Optional extension loading (lindera, ICU, etc.)
46
+ */
47
+ export class BetterSqlite3Backend extends EventEmitter {
48
+ config;
49
+ db = null;
50
+ initialized = false;
51
+ ftsAvailable = false;
52
+ // Performance tracking
53
+ stats = {
54
+ queryCount: 0,
55
+ totalQueryTime: 0,
56
+ writeCount: 0,
57
+ totalWriteTime: 0,
58
+ };
59
+ constructor(config = {}) {
60
+ super();
61
+ this.config = { ...DEFAULT_CONFIG, ...config };
62
+ }
63
+ /**
64
+ * Initialize the BetterSqlite3 backend
65
+ */
66
+ async initialize() {
67
+ if (this.initialized)
68
+ return;
69
+ // Dynamic import for better-sqlite3 (optional dependency)
70
+ let DatabaseConstructor;
71
+ try {
72
+ DatabaseConstructor = (await import('better-sqlite3')).default;
73
+ }
74
+ catch (error) {
75
+ throw new Error('better-sqlite3 is not installed. ' +
76
+ 'For production CJK support, run: npm install better-sqlite3');
77
+ }
78
+ // Ensure directory exists for file-based databases
79
+ if (this.config.databasePath !== ':memory:') {
80
+ const dir = path.dirname(this.config.databasePath);
81
+ if (dir && !existsSync(dir)) {
82
+ mkdirSync(dir, { recursive: true });
83
+ }
84
+ }
85
+ // Create database
86
+ this.db = new DatabaseConstructor(this.config.databasePath);
87
+ if (this.config.verbose) {
88
+ const versionRow = this.db.prepare('SELECT sqlite_version() as version').get();
89
+ console.log(`[BetterSqlite3] Opened database: ${this.config.databasePath}`);
90
+ console.log(`[BetterSqlite3] SQLite version: ${versionRow.version}`);
91
+ }
92
+ // Enable optimizations
93
+ if (this.config.optimize) {
94
+ this.db.pragma('journal_mode = WAL');
95
+ this.db.pragma('synchronous = NORMAL');
96
+ this.db.pragma('cache_size = -64000'); // 64MB cache
97
+ this.db.pragma('temp_store = MEMORY');
98
+ }
99
+ // Load custom extension if provided
100
+ if (this.config.extensionPath) {
101
+ try {
102
+ this.db.loadExtension(this.config.extensionPath);
103
+ if (this.config.verbose) {
104
+ console.log(`[BetterSqlite3] Loaded extension: ${this.config.extensionPath}`);
105
+ }
106
+ }
107
+ catch (error) {
108
+ console.warn(`[BetterSqlite3] Failed to load extension: ${error}`);
109
+ }
110
+ }
111
+ // Create schema
112
+ this.createSchema();
113
+ // Create FTS5 table with appropriate tokenizer
114
+ this.createFtsTable();
115
+ this.initialized = true;
116
+ this.emit('initialized');
117
+ if (this.config.verbose) {
118
+ console.log('[BetterSqlite3] Backend initialized');
119
+ console.log(`[BetterSqlite3] FTS5 available: ${this.ftsAvailable}`);
120
+ console.log(`[BetterSqlite3] Tokenizer: ${this.getActiveTokenizer()}`);
121
+ }
122
+ }
123
+ /**
124
+ * Create the database schema
125
+ */
126
+ createSchema() {
127
+ if (!this.db)
128
+ throw new Error('Database not initialized');
129
+ this.db.exec(`
130
+ CREATE TABLE IF NOT EXISTS memory_entries (
131
+ id TEXT PRIMARY KEY,
132
+ key TEXT NOT NULL,
133
+ content TEXT NOT NULL,
134
+ type TEXT DEFAULT 'semantic',
135
+ namespace TEXT DEFAULT 'default',
136
+ tags TEXT DEFAULT '[]',
137
+ metadata TEXT DEFAULT '{}',
138
+ embedding BLOB,
139
+ session_id TEXT,
140
+ owner_id TEXT,
141
+ access_level TEXT DEFAULT 'project',
142
+ created_at INTEGER NOT NULL,
143
+ updated_at INTEGER NOT NULL,
144
+ expires_at INTEGER,
145
+ version INTEGER DEFAULT 1,
146
+ "references" TEXT DEFAULT '[]',
147
+ access_count INTEGER DEFAULT 0,
148
+ last_accessed_at INTEGER NOT NULL
149
+ )
150
+ `);
151
+ // Create indexes
152
+ this.db.exec(`
153
+ CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace);
154
+ CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key);
155
+ CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type);
156
+ CREATE INDEX IF NOT EXISTS idx_created_at ON memory_entries(created_at);
157
+ CREATE INDEX IF NOT EXISTS idx_namespace_key ON memory_entries(namespace, key);
158
+ `);
159
+ }
160
+ /**
161
+ * Create FTS5 virtual table with appropriate tokenizer
162
+ */
163
+ createFtsTable() {
164
+ if (!this.db)
165
+ throw new Error('Database not initialized');
166
+ // Determine tokenizer configuration
167
+ let tokenizerConfig;
168
+ if (this.config.customTokenizer) {
169
+ tokenizerConfig = `tokenize='${this.config.customTokenizer}'`;
170
+ }
171
+ else {
172
+ switch (this.config.ftsTokenizer) {
173
+ case 'trigram':
174
+ tokenizerConfig = "tokenize='trigram'";
175
+ break;
176
+ case 'porter':
177
+ tokenizerConfig = "tokenize='porter unicode61'";
178
+ break;
179
+ default:
180
+ tokenizerConfig = "tokenize='unicode61'";
181
+ }
182
+ }
183
+ try {
184
+ // Create FTS5 virtual table
185
+ this.db.exec(`
186
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
187
+ key,
188
+ content,
189
+ namespace,
190
+ tags,
191
+ content=memory_entries,
192
+ content_rowid=rowid,
193
+ ${tokenizerConfig}
194
+ )
195
+ `);
196
+ // Create triggers to keep FTS in sync
197
+ this.db.exec(`
198
+ CREATE TRIGGER IF NOT EXISTS memory_fts_insert AFTER INSERT ON memory_entries BEGIN
199
+ INSERT INTO memory_fts(rowid, key, content, namespace, tags)
200
+ VALUES (NEW.rowid, NEW.key, NEW.content, NEW.namespace, NEW.tags);
201
+ END
202
+ `);
203
+ this.db.exec(`
204
+ CREATE TRIGGER IF NOT EXISTS memory_fts_delete AFTER DELETE ON memory_entries BEGIN
205
+ INSERT INTO memory_fts(memory_fts, rowid, key, content, namespace, tags)
206
+ VALUES ('delete', OLD.rowid, OLD.key, OLD.content, OLD.namespace, OLD.tags);
207
+ END
208
+ `);
209
+ this.db.exec(`
210
+ CREATE TRIGGER IF NOT EXISTS memory_fts_update AFTER UPDATE ON memory_entries BEGIN
211
+ INSERT INTO memory_fts(memory_fts, rowid, key, content, namespace, tags)
212
+ VALUES ('delete', OLD.rowid, OLD.key, OLD.content, OLD.namespace, OLD.tags);
213
+ INSERT INTO memory_fts(rowid, key, content, namespace, tags)
214
+ VALUES (NEW.rowid, NEW.key, NEW.content, NEW.namespace, NEW.tags);
215
+ END
216
+ `);
217
+ // Populate FTS from existing entries
218
+ this.db.exec(`
219
+ INSERT INTO memory_fts(memory_fts) VALUES('rebuild')
220
+ `);
221
+ this.ftsAvailable = true;
222
+ if (this.config.verbose) {
223
+ console.log(`[BetterSqlite3] FTS5 initialized with ${tokenizerConfig}`);
224
+ }
225
+ }
226
+ catch (error) {
227
+ console.warn(`[BetterSqlite3] FTS5 initialization failed: ${error}`);
228
+ this.ftsAvailable = false;
229
+ }
230
+ }
231
+ /**
232
+ * Get the active tokenizer being used
233
+ */
234
+ getActiveTokenizer() {
235
+ if (this.config.customTokenizer) {
236
+ return this.config.customTokenizer;
237
+ }
238
+ return this.config.ftsTokenizer;
239
+ }
240
+ /**
241
+ * Check if FTS5 is available and CJK optimized
242
+ */
243
+ isFtsAvailable() {
244
+ return this.ftsAvailable;
245
+ }
246
+ /**
247
+ * Check if CJK is optimally supported (trigram or lindera)
248
+ */
249
+ isCjkOptimized() {
250
+ return this.ftsAvailable && (this.config.ftsTokenizer === 'trigram' ||
251
+ this.config.customTokenizer?.includes('lindera') === true);
252
+ }
253
+ /**
254
+ * Shutdown the backend
255
+ */
256
+ async shutdown() {
257
+ if (this.db) {
258
+ this.db.close();
259
+ this.db = null;
260
+ }
261
+ this.initialized = false;
262
+ this.emit('shutdown');
263
+ }
264
+ /**
265
+ * Store a memory entry
266
+ */
267
+ async store(entry) {
268
+ if (!this.db)
269
+ throw new Error('Database not initialized');
270
+ const startTime = Date.now();
271
+ const stmt = this.db.prepare(`
272
+ INSERT OR REPLACE INTO memory_entries
273
+ (id, key, content, type, namespace, tags, metadata, embedding, session_id,
274
+ owner_id, access_level, created_at, updated_at, expires_at, version,
275
+ "references", access_count, last_accessed_at)
276
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
277
+ `);
278
+ stmt.run(entry.id, entry.key, entry.content, entry.type, entry.namespace, JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.embedding ? Buffer.from(entry.embedding.buffer) : null, entry.sessionId || null, entry.ownerId || null, entry.accessLevel, entry.createdAt, entry.updatedAt, entry.expiresAt || null, entry.version, JSON.stringify(entry.references), entry.accessCount, entry.lastAccessedAt);
279
+ const duration = Date.now() - startTime;
280
+ this.stats.writeCount++;
281
+ this.stats.totalWriteTime += duration;
282
+ this.emit('entry:stored', { entry, duration });
283
+ }
284
+ /**
285
+ * Retrieve a memory entry by ID
286
+ */
287
+ async get(id) {
288
+ if (!this.db)
289
+ throw new Error('Database not initialized');
290
+ const startTime = Date.now();
291
+ const stmt = this.db.prepare('SELECT * FROM memory_entries WHERE id = ?');
292
+ const row = stmt.get(id);
293
+ this.stats.queryCount++;
294
+ this.stats.totalQueryTime += Date.now() - startTime;
295
+ if (!row)
296
+ return null;
297
+ // Update access count
298
+ this.db.prepare('UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?').run(Date.now(), id);
299
+ return this.rowToEntry(row);
300
+ }
301
+ /**
302
+ * Retrieve a memory entry by key within a namespace
303
+ */
304
+ async getByKey(namespace, key) {
305
+ if (!this.db)
306
+ throw new Error('Database not initialized');
307
+ const stmt = this.db.prepare('SELECT * FROM memory_entries WHERE namespace = ? AND key = ?');
308
+ const row = stmt.get(namespace, key);
309
+ if (!row)
310
+ return null;
311
+ // Update access count
312
+ this.db.prepare('UPDATE memory_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?').run(Date.now(), row.id);
313
+ return this.rowToEntry(row);
314
+ }
315
+ /**
316
+ * Update a memory entry
317
+ */
318
+ async update(id, update) {
319
+ if (!this.db)
320
+ throw new Error('Database not initialized');
321
+ const existing = await this.get(id);
322
+ if (!existing)
323
+ return null;
324
+ const updated = {
325
+ ...existing,
326
+ ...update,
327
+ updatedAt: Date.now(),
328
+ version: existing.version + 1,
329
+ };
330
+ await this.store(updated);
331
+ return updated;
332
+ }
333
+ /**
334
+ * Delete a memory entry
335
+ */
336
+ async delete(id) {
337
+ if (!this.db)
338
+ throw new Error('Database not initialized');
339
+ const result = this.db.prepare('DELETE FROM memory_entries WHERE id = ?').run(id);
340
+ return result.changes > 0;
341
+ }
342
+ /**
343
+ * Query memory entries
344
+ */
345
+ async query(query) {
346
+ if (!this.db)
347
+ throw new Error('Database not initialized');
348
+ const startTime = Date.now();
349
+ const conditions = [];
350
+ const params = [];
351
+ if (query.namespace) {
352
+ conditions.push('namespace = ?');
353
+ params.push(query.namespace);
354
+ }
355
+ if (query.memoryType) {
356
+ conditions.push('type = ?');
357
+ params.push(query.memoryType);
358
+ }
359
+ if (query.tags && query.tags.length > 0) {
360
+ const tagConditions = query.tags.map(() => 'tags LIKE ?');
361
+ conditions.push(`(${tagConditions.join(' OR ')})`);
362
+ query.tags.forEach((tag) => params.push(`%"${tag}"%`));
363
+ }
364
+ if (query.createdBefore) {
365
+ conditions.push('created_at < ?');
366
+ params.push(query.createdBefore);
367
+ }
368
+ if (query.createdAfter) {
369
+ conditions.push('created_at > ?');
370
+ params.push(query.createdAfter);
371
+ }
372
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
373
+ const limit = query.limit || 100;
374
+ const sql = `
375
+ SELECT * FROM memory_entries
376
+ ${whereClause}
377
+ ORDER BY created_at DESC
378
+ LIMIT ?
379
+ `;
380
+ params.push(limit);
381
+ const rows = this.db.prepare(sql).all(...params);
382
+ this.stats.queryCount++;
383
+ this.stats.totalQueryTime += Date.now() - startTime;
384
+ return rows.map((row) => this.rowToEntry(row));
385
+ }
386
+ /**
387
+ * Full-text search using FTS5
388
+ */
389
+ async searchFts(query, options = {}) {
390
+ if (!this.db)
391
+ throw new Error('Database not initialized');
392
+ if (!this.ftsAvailable) {
393
+ // Fall back to LIKE search
394
+ return this.searchLike(query, options);
395
+ }
396
+ const limit = options.limit || 100;
397
+ const params = [];
398
+ // Sanitize query for FTS5
399
+ const sanitizedQuery = this.sanitizeFtsQuery(query);
400
+ if (!sanitizedQuery)
401
+ return [];
402
+ let sql;
403
+ if (options.namespace) {
404
+ sql = `
405
+ SELECT m.*, bm25(memory_fts) as rank
406
+ FROM memory_fts f
407
+ JOIN memory_entries m ON f.rowid = m.rowid
408
+ WHERE memory_fts MATCH ? AND m.namespace = ?
409
+ ORDER BY rank
410
+ LIMIT ?
411
+ `;
412
+ params.push(sanitizedQuery, options.namespace, limit);
413
+ }
414
+ else {
415
+ sql = `
416
+ SELECT m.*, bm25(memory_fts) as rank
417
+ FROM memory_fts f
418
+ JOIN memory_entries m ON f.rowid = m.rowid
419
+ WHERE memory_fts MATCH ?
420
+ ORDER BY rank
421
+ LIMIT ?
422
+ `;
423
+ params.push(sanitizedQuery, limit);
424
+ }
425
+ try {
426
+ const rows = this.db.prepare(sql).all(...params);
427
+ return rows.map((row) => this.rowToEntry(row));
428
+ }
429
+ catch (error) {
430
+ // Fall back to LIKE on error
431
+ console.warn(`[BetterSqlite3] FTS5 search failed, falling back to LIKE: ${error}`);
432
+ return this.searchLike(query, options);
433
+ }
434
+ }
435
+ /**
436
+ * LIKE-based search fallback
437
+ */
438
+ searchLike(query, options) {
439
+ if (!this.db)
440
+ throw new Error('Database not initialized');
441
+ const pattern = `%${query}%`;
442
+ const limit = options.limit || 100;
443
+ let sql;
444
+ const params = [];
445
+ if (options.namespace) {
446
+ sql = `
447
+ SELECT * FROM memory_entries
448
+ WHERE (content LIKE ? OR key LIKE ? OR tags LIKE ?) AND namespace = ?
449
+ ORDER BY created_at DESC
450
+ LIMIT ?
451
+ `;
452
+ params.push(pattern, pattern, pattern, options.namespace, limit);
453
+ }
454
+ else {
455
+ sql = `
456
+ SELECT * FROM memory_entries
457
+ WHERE content LIKE ? OR key LIKE ? OR tags LIKE ?
458
+ ORDER BY created_at DESC
459
+ LIMIT ?
460
+ `;
461
+ params.push(pattern, pattern, pattern, limit);
462
+ }
463
+ const rows = this.db.prepare(sql).all(...params);
464
+ return rows.map((row) => this.rowToEntry(row));
465
+ }
466
+ /**
467
+ * Sanitize query for FTS5
468
+ */
469
+ sanitizeFtsQuery(query) {
470
+ // Remove special FTS5 operators and wrap terms in quotes
471
+ return query
472
+ .replace(/[^\w\s\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u3400-\u4DBF]/g, ' ')
473
+ .split(/\s+/)
474
+ .filter((term) => term.length > 0)
475
+ .map((term) => `"${term}"`)
476
+ .join(' OR ');
477
+ }
478
+ /**
479
+ * Semantic vector search
480
+ */
481
+ async search(embedding, options) {
482
+ if (!this.db)
483
+ throw new Error('Database not initialized');
484
+ const startTime = Date.now();
485
+ // Get all entries with embeddings
486
+ let sql = `
487
+ SELECT * FROM memory_entries
488
+ WHERE embedding IS NOT NULL
489
+ `;
490
+ const params = [];
491
+ // Apply filters if provided
492
+ if (options.filters?.namespace) {
493
+ sql += ' AND namespace = ?';
494
+ params.push(options.filters.namespace);
495
+ }
496
+ if (options.filters?.memoryType) {
497
+ sql += ' AND type = ?';
498
+ params.push(options.filters.memoryType);
499
+ }
500
+ const rows = this.db.prepare(sql).all(...params);
501
+ // Calculate cosine similarity
502
+ const results = rows
503
+ .map((row) => {
504
+ const entry = this.rowToEntry(row);
505
+ if (!entry.embedding)
506
+ return null;
507
+ const similarity = this.cosineSimilarity(embedding, entry.embedding);
508
+ const distance = 1 - similarity;
509
+ // Apply threshold (similarity threshold, not distance)
510
+ if (options.threshold !== undefined && similarity < options.threshold) {
511
+ return null;
512
+ }
513
+ return { entry, score: similarity, distance };
514
+ })
515
+ .filter((r) => r !== null);
516
+ // Sort by score and limit
517
+ results.sort((a, b) => b.score - a.score);
518
+ this.stats.queryCount++;
519
+ this.stats.totalQueryTime += Date.now() - startTime;
520
+ return results.slice(0, options.k);
521
+ }
522
+ /**
523
+ * Calculate cosine similarity between two vectors
524
+ */
525
+ cosineSimilarity(a, b) {
526
+ if (a.length !== b.length)
527
+ return 0;
528
+ let dotProduct = 0;
529
+ let normA = 0;
530
+ let normB = 0;
531
+ for (let i = 0; i < a.length; i++) {
532
+ dotProduct += a[i] * b[i];
533
+ normA += a[i] * a[i];
534
+ normB += b[i] * b[i];
535
+ }
536
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
537
+ return denominator === 0 ? 0 : dotProduct / denominator;
538
+ }
539
+ /**
540
+ * Bulk insert entries
541
+ */
542
+ async bulkInsert(entries) {
543
+ if (!this.db)
544
+ throw new Error('Database not initialized');
545
+ if (entries.length === 0)
546
+ return;
547
+ const stmt = this.db.prepare(`
548
+ INSERT OR REPLACE INTO memory_entries
549
+ (id, key, content, type, namespace, tags, metadata, embedding, session_id,
550
+ owner_id, access_level, created_at, updated_at, expires_at, version,
551
+ "references", access_count, last_accessed_at)
552
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
553
+ `);
554
+ const insert = this.db.transaction((entries) => {
555
+ for (const entry of entries) {
556
+ stmt.run(entry.id, entry.key, entry.content, entry.type, entry.namespace, JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.embedding ? Buffer.from(entry.embedding.buffer) : null, entry.sessionId || null, entry.ownerId || null, entry.accessLevel, entry.createdAt, entry.updatedAt, entry.expiresAt || null, entry.version, JSON.stringify(entry.references), entry.accessCount, entry.lastAccessedAt);
557
+ }
558
+ });
559
+ insert(entries);
560
+ this.emit('bulkInserted', entries.length);
561
+ }
562
+ /**
563
+ * Bulk delete entries
564
+ */
565
+ async bulkDelete(ids) {
566
+ if (!this.db)
567
+ throw new Error('Database not initialized');
568
+ if (ids.length === 0)
569
+ return 0;
570
+ const placeholders = ids.map(() => '?').join(', ');
571
+ const result = this.db
572
+ .prepare(`DELETE FROM memory_entries WHERE id IN (${placeholders})`)
573
+ .run(...ids);
574
+ return result.changes;
575
+ }
576
+ /**
577
+ * Get entry count
578
+ */
579
+ async count(namespace) {
580
+ if (!this.db)
581
+ throw new Error('Database not initialized');
582
+ if (namespace) {
583
+ const result = this.db
584
+ .prepare('SELECT COUNT(*) as count FROM memory_entries WHERE namespace = ?')
585
+ .get(namespace);
586
+ return result.count;
587
+ }
588
+ const result = this.db
589
+ .prepare('SELECT COUNT(*) as count FROM memory_entries')
590
+ .get();
591
+ return result.count;
592
+ }
593
+ /**
594
+ * List all namespaces
595
+ */
596
+ async listNamespaces() {
597
+ if (!this.db)
598
+ throw new Error('Database not initialized');
599
+ const rows = this.db
600
+ .prepare('SELECT DISTINCT namespace FROM memory_entries')
601
+ .all();
602
+ return rows.map((r) => r.namespace);
603
+ }
604
+ /**
605
+ * Clear all entries in a namespace
606
+ */
607
+ async clearNamespace(namespace) {
608
+ if (!this.db)
609
+ throw new Error('Database not initialized');
610
+ const result = this.db
611
+ .prepare('DELETE FROM memory_entries WHERE namespace = ?')
612
+ .run(namespace);
613
+ return result.changes;
614
+ }
615
+ /**
616
+ * Get backend statistics
617
+ */
618
+ async getStats() {
619
+ if (!this.db)
620
+ throw new Error('Database not initialized');
621
+ const totalEntries = await this.count();
622
+ const namespaces = await this.listNamespaces();
623
+ const entriesByNamespace = {};
624
+ for (const ns of namespaces) {
625
+ entriesByNamespace[ns] = await this.count(ns);
626
+ }
627
+ // Get database size
628
+ const pageCount = this.db.pragma('page_count', { simple: true });
629
+ const pageSize = this.db.pragma('page_size', { simple: true });
630
+ const dbSizeBytes = pageCount * pageSize;
631
+ // Get type breakdown
632
+ const typeRows = this.db.prepare(`
633
+ SELECT type, COUNT(*) as count FROM memory_entries GROUP BY type
634
+ `).all();
635
+ const entriesByType = {
636
+ episodic: 0,
637
+ semantic: 0,
638
+ procedural: 0,
639
+ working: 0,
640
+ cache: 0,
641
+ };
642
+ for (const row of typeRows) {
643
+ entriesByType[row.type] = row.count;
644
+ }
645
+ return {
646
+ totalEntries,
647
+ entriesByNamespace,
648
+ entriesByType,
649
+ memoryUsage: dbSizeBytes,
650
+ avgQueryTime: this.stats.queryCount > 0 ? this.stats.totalQueryTime / this.stats.queryCount : 0,
651
+ avgSearchTime: this.stats.queryCount > 0 ? this.stats.totalQueryTime / this.stats.queryCount : 0,
652
+ };
653
+ }
654
+ /**
655
+ * Perform health check
656
+ */
657
+ async healthCheck() {
658
+ const issues = [];
659
+ const recommendations = [];
660
+ // Check database (storage)
661
+ let storageHealth;
662
+ const storageStart = Date.now();
663
+ try {
664
+ if (this.db) {
665
+ this.db.prepare('SELECT 1').get();
666
+ storageHealth = {
667
+ status: 'healthy',
668
+ latency: Date.now() - storageStart,
669
+ };
670
+ }
671
+ else {
672
+ storageHealth = {
673
+ status: 'unhealthy',
674
+ latency: 0,
675
+ message: 'Database not initialized',
676
+ };
677
+ issues.push('Database not initialized');
678
+ }
679
+ }
680
+ catch (error) {
681
+ storageHealth = {
682
+ status: 'unhealthy',
683
+ latency: Date.now() - storageStart,
684
+ message: String(error),
685
+ };
686
+ issues.push(`Database error: ${error}`);
687
+ }
688
+ // Check FTS5 (index)
689
+ const indexHealth = {
690
+ status: this.ftsAvailable ? 'healthy' : 'degraded',
691
+ latency: 0,
692
+ message: this.ftsAvailable
693
+ ? `Tokenizer: ${this.getActiveTokenizer()}`
694
+ : 'FTS5 not available, using LIKE fallback',
695
+ };
696
+ if (!this.ftsAvailable) {
697
+ recommendations.push('Enable FTS5 for better search performance');
698
+ }
699
+ // Check CJK optimization (cache - repurpose for CJK status)
700
+ const cacheHealth = {
701
+ status: this.isCjkOptimized() ? 'healthy' : 'degraded',
702
+ latency: 0,
703
+ message: this.isCjkOptimized()
704
+ ? 'Trigram tokenizer active for CJK'
705
+ : 'CJK using fallback search',
706
+ };
707
+ if (!this.isCjkOptimized()) {
708
+ recommendations.push('Use trigram tokenizer for proper CJK (Japanese/Chinese/Korean) support');
709
+ }
710
+ // Determine overall status
711
+ let status;
712
+ if (storageHealth.status === 'unhealthy') {
713
+ status = 'unhealthy';
714
+ }
715
+ else if (indexHealth.status === 'degraded' || cacheHealth.status === 'degraded') {
716
+ status = 'degraded';
717
+ }
718
+ else {
719
+ status = 'healthy';
720
+ }
721
+ return {
722
+ status,
723
+ timestamp: Date.now(),
724
+ components: {
725
+ storage: storageHealth,
726
+ index: indexHealth,
727
+ cache: cacheHealth,
728
+ },
729
+ issues,
730
+ recommendations,
731
+ };
732
+ }
733
+ /**
734
+ * Get the underlying database for advanced operations
735
+ */
736
+ getDatabase() {
737
+ return this.db;
738
+ }
739
+ /**
740
+ * Rebuild FTS index
741
+ */
742
+ async rebuildFtsIndex() {
743
+ if (!this.db || !this.ftsAvailable)
744
+ return;
745
+ this.db.exec("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')");
746
+ if (this.config.verbose) {
747
+ console.log('[BetterSqlite3] FTS index rebuilt');
748
+ }
749
+ }
750
+ /**
751
+ * Convert database row to MemoryEntry
752
+ */
753
+ rowToEntry(row) {
754
+ let embedding;
755
+ if (row.embedding) {
756
+ const buffer = row.embedding;
757
+ embedding = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
758
+ }
759
+ return {
760
+ id: row.id,
761
+ key: row.key,
762
+ content: row.content,
763
+ embedding,
764
+ type: row.type,
765
+ namespace: row.namespace,
766
+ tags: JSON.parse(row.tags || '[]'),
767
+ metadata: JSON.parse(row.metadata || '{}'),
768
+ sessionId: row.session_id,
769
+ ownerId: row.owner_id,
770
+ accessLevel: row.access_level,
771
+ createdAt: row.created_at,
772
+ updatedAt: row.updated_at,
773
+ expiresAt: row.expires_at,
774
+ version: row.version,
775
+ references: JSON.parse(row.references || '[]'),
776
+ accessCount: row.access_count,
777
+ lastAccessedAt: row.last_accessed_at,
778
+ };
779
+ }
780
+ }
781
+ /**
782
+ * Create a BetterSqlite3 backend with default CJK support
783
+ */
784
+ export function createBetterSqlite3Backend(config) {
785
+ return new BetterSqlite3Backend({
786
+ ...config,
787
+ ftsTokenizer: config?.ftsTokenizer || 'trigram', // Default to trigram for CJK
788
+ });
789
+ }
790
+ /**
791
+ * Create a BetterSqlite3 backend with lindera extension for advanced Japanese
792
+ */
793
+ export function createJapaneseOptimizedBackend(config) {
794
+ return new BetterSqlite3Backend({
795
+ ...config,
796
+ extensionPath: config.linderaPath,
797
+ customTokenizer: 'lindera_tokenizer',
798
+ });
799
+ }
800
+ export default BetterSqlite3Backend;
801
+ //# sourceMappingURL=better-sqlite3-backend.js.map