openclaw-mem 1.0.4 → 1.3.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 (47) hide show
  1. package/HOOK.md +125 -0
  2. package/LICENSE +1 -1
  3. package/MCP.json +11 -0
  4. package/README.md +158 -167
  5. package/backfill-embeddings.js +79 -0
  6. package/context-builder.js +703 -0
  7. package/database.js +625 -0
  8. package/debug-logger.js +280 -0
  9. package/extractor.js +268 -0
  10. package/gateway-llm.js +250 -0
  11. package/handler.js +941 -0
  12. package/mcp-http-api.js +424 -0
  13. package/mcp-server.js +605 -0
  14. package/mem-get.sh +24 -0
  15. package/mem-search.sh +17 -0
  16. package/monitor.js +112 -0
  17. package/package.json +58 -30
  18. package/realtime-monitor.js +371 -0
  19. package/session-watcher.js +192 -0
  20. package/setup.js +114 -0
  21. package/sync-recent.js +63 -0
  22. package/README_CN.md +0 -201
  23. package/bin/openclaw-mem.js +0 -117
  24. package/docs/locales/README_AR.md +0 -35
  25. package/docs/locales/README_DE.md +0 -35
  26. package/docs/locales/README_ES.md +0 -35
  27. package/docs/locales/README_FR.md +0 -35
  28. package/docs/locales/README_HE.md +0 -35
  29. package/docs/locales/README_HI.md +0 -35
  30. package/docs/locales/README_ID.md +0 -35
  31. package/docs/locales/README_IT.md +0 -35
  32. package/docs/locales/README_JA.md +0 -57
  33. package/docs/locales/README_KO.md +0 -35
  34. package/docs/locales/README_NL.md +0 -35
  35. package/docs/locales/README_PL.md +0 -35
  36. package/docs/locales/README_PT.md +0 -35
  37. package/docs/locales/README_RU.md +0 -35
  38. package/docs/locales/README_TH.md +0 -35
  39. package/docs/locales/README_TR.md +0 -35
  40. package/docs/locales/README_UK.md +0 -35
  41. package/docs/locales/README_VI.md +0 -35
  42. package/docs/logo.svg +0 -32
  43. package/lib/context-builder.js +0 -415
  44. package/lib/database.js +0 -309
  45. package/lib/handler.js +0 -494
  46. package/scripts/commands.js +0 -141
  47. package/scripts/init.js +0 -248
package/database.js ADDED
@@ -0,0 +1,625 @@
1
+ /**
2
+ * OpenClaw-Mem Database Module
3
+ * SQLite-based storage for observations, sessions, and summaries
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import Database from 'better-sqlite3';
10
+ import * as sqliteVec from 'sqlite-vec';
11
+
12
+ const DATA_DIR = path.join(os.homedir(), '.openclaw-mem');
13
+ const DB_PATH = path.join(DATA_DIR, 'memory.db');
14
+
15
+ // Ensure data directory exists
16
+ if (!fs.existsSync(DATA_DIR)) {
17
+ fs.mkdirSync(DATA_DIR, { recursive: true });
18
+ }
19
+
20
+ // Initialize database
21
+ const db = new Database(DB_PATH);
22
+ db.pragma('journal_mode = WAL');
23
+
24
+ // Load sqlite-vec extension for vector search
25
+ try {
26
+ sqliteVec.load(db);
27
+ console.log('[openclaw-mem] sqlite-vec extension loaded');
28
+ } catch (e) {
29
+ console.error('[openclaw-mem] Failed to load sqlite-vec:', e.message);
30
+ }
31
+
32
+ // Create tables (base schema without new columns for backward compatibility)
33
+ db.exec(`
34
+ -- Sessions table
35
+ CREATE TABLE IF NOT EXISTS sessions (
36
+ id TEXT PRIMARY KEY,
37
+ project_path TEXT,
38
+ session_key TEXT,
39
+ started_at TEXT DEFAULT (datetime('now')),
40
+ ended_at TEXT,
41
+ status TEXT DEFAULT 'active',
42
+ source TEXT
43
+ );
44
+
45
+ -- Observations table (tool calls) - base schema
46
+ CREATE TABLE IF NOT EXISTS observations (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ session_id TEXT,
49
+ timestamp TEXT DEFAULT (datetime('now')),
50
+ tool_name TEXT NOT NULL,
51
+ tool_input TEXT,
52
+ tool_response TEXT,
53
+ summary TEXT,
54
+ concepts TEXT,
55
+ tokens_discovery INTEGER DEFAULT 0,
56
+ tokens_read INTEGER DEFAULT 0,
57
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
58
+ );
59
+
60
+ -- User prompts table (for tracking user inputs)
61
+ CREATE TABLE IF NOT EXISTS user_prompts (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ session_id TEXT,
64
+ content TEXT,
65
+ timestamp TEXT DEFAULT (datetime('now')),
66
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
67
+ );
68
+
69
+ -- Summaries table
70
+ CREATE TABLE IF NOT EXISTS summaries (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ session_id TEXT,
73
+ content TEXT,
74
+ request TEXT,
75
+ learned TEXT,
76
+ completed TEXT,
77
+ next_steps TEXT,
78
+ created_at TEXT DEFAULT (datetime('now')),
79
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
80
+ );
81
+
82
+ -- Base indexes
83
+ CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
84
+ CREATE INDEX IF NOT EXISTS idx_observations_timestamp ON observations(timestamp DESC);
85
+ CREATE INDEX IF NOT EXISTS idx_observations_tool ON observations(tool_name);
86
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
87
+ CREATE INDEX IF NOT EXISTS idx_user_prompts_session ON user_prompts(session_id);
88
+ `);
89
+
90
+ // Migrate existing database - add new columns if they don't exist
91
+ // Must be done before creating indexes on these columns
92
+ const migrations = [
93
+ `ALTER TABLE observations ADD COLUMN type TEXT`,
94
+ `ALTER TABLE observations ADD COLUMN narrative TEXT`,
95
+ `ALTER TABLE observations ADD COLUMN facts TEXT`,
96
+ `ALTER TABLE observations ADD COLUMN files_read TEXT`,
97
+ `ALTER TABLE observations ADD COLUMN files_modified TEXT`,
98
+ `ALTER TABLE summaries ADD COLUMN learned TEXT`
99
+ ];
100
+
101
+ for (const migration of migrations) {
102
+ try {
103
+ db.exec(migration);
104
+ } catch (e) {
105
+ // Column already exists, ignore
106
+ }
107
+ }
108
+
109
+ // Create index on type column after migration
110
+ try {
111
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type)`);
112
+ } catch (e) {
113
+ // Index might already exist
114
+ }
115
+
116
+ // Create/recreate FTS5 table with extended fields
117
+ // Drop old triggers first to avoid conflicts
118
+ try {
119
+ db.exec(`DROP TRIGGER IF EXISTS observations_ai`);
120
+ db.exec(`DROP TRIGGER IF EXISTS observations_ad`);
121
+ db.exec(`DROP TRIGGER IF EXISTS observations_au`);
122
+ } catch (e) { /* triggers don't exist */ }
123
+
124
+ // Check if FTS table needs to be recreated with new columns
125
+ const ftsInfo = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='observations_fts'`).get();
126
+ const needsRecreate = ftsInfo && !ftsInfo.sql.includes('narrative');
127
+
128
+ if (needsRecreate) {
129
+ try {
130
+ db.exec(`DROP TABLE IF EXISTS observations_fts`);
131
+ } catch (e) { /* table doesn't exist */ }
132
+ }
133
+
134
+ // Create FTS5 table with extended fields
135
+ db.exec(`
136
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
137
+ tool_name,
138
+ summary,
139
+ concepts,
140
+ narrative,
141
+ facts,
142
+ content='observations',
143
+ content_rowid='id'
144
+ );
145
+ `);
146
+
147
+ // Rebuild FTS index if we recreated the table
148
+ if (needsRecreate) {
149
+ try {
150
+ const allObs = db.prepare(`SELECT id, tool_name, summary, concepts, narrative, facts FROM observations`).all();
151
+ const insertFts = db.prepare(`INSERT INTO observations_fts(rowid, tool_name, summary, concepts, narrative, facts) VALUES (?, ?, ?, ?, ?, ?)`);
152
+ for (const obs of allObs) {
153
+ insertFts.run(obs.id, obs.tool_name, obs.summary, obs.concepts, obs.narrative, obs.facts);
154
+ }
155
+ } catch (e) {
156
+ console.error('[openclaw-mem] FTS rebuild error:', e.message);
157
+ }
158
+ }
159
+
160
+ // Create triggers for FTS sync
161
+ db.exec(`
162
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
163
+ INSERT INTO observations_fts(rowid, tool_name, summary, concepts, narrative, facts)
164
+ VALUES (new.id, new.tool_name, new.summary, new.concepts, new.narrative, new.facts);
165
+ END;
166
+
167
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
168
+ INSERT INTO observations_fts(observations_fts, rowid, tool_name, summary, concepts, narrative, facts)
169
+ VALUES ('delete', old.id, old.tool_name, old.summary, old.concepts, old.narrative, old.facts);
170
+ END;
171
+
172
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
173
+ INSERT INTO observations_fts(observations_fts, rowid, tool_name, summary, concepts, narrative, facts)
174
+ VALUES ('delete', old.id, old.tool_name, old.summary, old.concepts, old.narrative, old.facts);
175
+ INSERT INTO observations_fts(rowid, tool_name, summary, concepts, narrative, facts)
176
+ VALUES (new.id, new.tool_name, new.summary, new.concepts, new.narrative, new.facts);
177
+ END;
178
+ `);
179
+
180
+ // Create vec0 virtual table for vector embeddings
181
+ // Drop and recreate if dimension mismatch (migration from 768/1024 to 384)
182
+ try {
183
+ const vecInfo = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='observation_embeddings'`).get();
184
+ if (vecInfo && !vecInfo.sql.includes('float[384]')) {
185
+ console.log('[openclaw-mem] Recreating vec0 table with 384 dimensions...');
186
+ db.exec(`DROP TABLE IF EXISTS observation_embeddings`);
187
+ }
188
+ } catch (e) { /* table doesn't exist yet */ }
189
+
190
+ try {
191
+ db.exec(`
192
+ CREATE VIRTUAL TABLE IF NOT EXISTS observation_embeddings USING vec0(
193
+ observation_id INTEGER PRIMARY KEY,
194
+ embedding float[384]
195
+ );
196
+ `);
197
+ console.log('[openclaw-mem] observation_embeddings vec0 table ready');
198
+ } catch (e) {
199
+ console.error('[openclaw-mem] Failed to create vec0 table:', e.message);
200
+ }
201
+
202
+ // Prepared statements
203
+ const stmts = {
204
+ // Sessions
205
+ createSession: db.prepare(`
206
+ INSERT INTO sessions (id, project_path, session_key, source)
207
+ VALUES (?, ?, ?, ?)
208
+ `),
209
+
210
+ getSession: db.prepare(`
211
+ SELECT * FROM sessions WHERE id = ?
212
+ `),
213
+
214
+ endSession: db.prepare(`
215
+ UPDATE sessions SET ended_at = datetime('now'), status = 'completed'
216
+ WHERE id = ?
217
+ `),
218
+
219
+ getActiveSession: db.prepare(`
220
+ SELECT * FROM sessions WHERE session_key = ? AND status = 'active'
221
+ ORDER BY started_at DESC LIMIT 1
222
+ `),
223
+
224
+ // Observations - extended with type, narrative, facts, files tracking
225
+ saveObservation: db.prepare(`
226
+ INSERT INTO observations (session_id, tool_name, tool_input, tool_response, summary, concepts, tokens_discovery, tokens_read, type, narrative, facts, files_read, files_modified)
227
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
228
+ `),
229
+
230
+ getObservation: db.prepare(`
231
+ SELECT * FROM observations WHERE id = ?
232
+ `),
233
+
234
+ getObservations: db.prepare(`
235
+ SELECT * FROM observations WHERE id IN (SELECT value FROM json_each(?))
236
+ `),
237
+
238
+ updateObservationSummary: db.prepare(`
239
+ UPDATE observations SET summary = ?, concepts = ?, tokens_read = ?
240
+ WHERE id = ?
241
+ `),
242
+
243
+ getRecentObservations: db.prepare(`
244
+ SELECT o.*, s.project_path
245
+ FROM observations o
246
+ JOIN sessions s ON o.session_id = s.id
247
+ WHERE s.project_path = ?
248
+ ORDER BY o.timestamp DESC
249
+ LIMIT ?
250
+ `),
251
+
252
+ getRecentObservationsAll: db.prepare(`
253
+ SELECT o.*, s.project_path
254
+ FROM observations o
255
+ JOIN sessions s ON o.session_id = s.id
256
+ ORDER BY o.timestamp DESC
257
+ LIMIT ?
258
+ `),
259
+
260
+ getObservationsByType: db.prepare(`
261
+ SELECT o.*, s.project_path
262
+ FROM observations o
263
+ JOIN sessions s ON o.session_id = s.id
264
+ WHERE o.type = ?
265
+ ORDER BY o.timestamp DESC
266
+ LIMIT ?
267
+ `),
268
+
269
+ searchObservations: db.prepare(`
270
+ SELECT o.*, s.project_path,
271
+ highlight(observations_fts, 1, '<mark>', '</mark>') as summary_highlight
272
+ FROM observations_fts fts
273
+ JOIN observations o ON fts.rowid = o.id
274
+ JOIN sessions s ON o.session_id = s.id
275
+ WHERE observations_fts MATCH ?
276
+ ORDER BY rank
277
+ LIMIT ?
278
+ `),
279
+
280
+ // User prompts
281
+ saveUserPrompt: db.prepare(`
282
+ INSERT INTO user_prompts (session_id, content)
283
+ VALUES (?, ?)
284
+ `),
285
+
286
+ getRecentUserPrompts: db.prepare(`
287
+ SELECT * FROM user_prompts
288
+ WHERE session_id = ?
289
+ ORDER BY timestamp DESC
290
+ LIMIT ?
291
+ `),
292
+
293
+ // Summaries
294
+ saveSummary: db.prepare(`
295
+ INSERT INTO summaries (session_id, content, request, investigated, learned, completed, next_steps)
296
+ VALUES (?, ?, ?, ?, ?, ?, ?)
297
+ `),
298
+
299
+ getRecentSummaries: db.prepare(`
300
+ SELECT su.*, s.project_path
301
+ FROM summaries su
302
+ JOIN sessions s ON su.session_id = s.id
303
+ WHERE s.project_path = ?
304
+ ORDER BY su.created_at DESC
305
+ LIMIT ?
306
+ `),
307
+
308
+ getSummaryBySession: db.prepare(`
309
+ SELECT * FROM summaries
310
+ WHERE session_id = ?
311
+ ORDER BY id DESC
312
+ LIMIT 1
313
+ `),
314
+
315
+ getSummaryBySessionKey: db.prepare(`
316
+ SELECT su.*, s.session_key
317
+ FROM summaries su
318
+ JOIN sessions s ON su.session_id = s.id
319
+ WHERE s.session_key = ?
320
+ ORDER BY su.id DESC
321
+ LIMIT 1
322
+ `),
323
+
324
+ // Embedding operations
325
+ saveEmbedding: db.prepare(`
326
+ INSERT OR REPLACE INTO observation_embeddings (observation_id, embedding)
327
+ VALUES (?, ?)
328
+ `),
329
+
330
+ searchByVector: db.prepare(`
331
+ SELECT observation_id, distance
332
+ FROM observation_embeddings
333
+ WHERE embedding MATCH ?
334
+ AND k = ?
335
+ ORDER BY distance
336
+ `),
337
+
338
+ getEmbeddingCount: db.prepare(`
339
+ SELECT COUNT(*) as count FROM observation_embeddings
340
+ `),
341
+
342
+ getObservationsWithoutEmbeddings: db.prepare(`
343
+ SELECT o.id, o.summary, o.narrative
344
+ FROM observations o
345
+ LEFT JOIN observation_embeddings oe ON o.id = oe.observation_id
346
+ WHERE oe.observation_id IS NULL
347
+ AND (o.summary IS NOT NULL OR o.narrative IS NOT NULL)
348
+ ORDER BY o.id
349
+ LIMIT ?
350
+ `),
351
+
352
+ // Stats
353
+ getStats: db.prepare(`
354
+ SELECT
355
+ (SELECT COUNT(*) FROM sessions) as total_sessions,
356
+ (SELECT COUNT(*) FROM observations) as total_observations,
357
+ (SELECT COUNT(*) FROM summaries) as total_summaries,
358
+ (SELECT COUNT(*) FROM user_prompts) as total_user_prompts,
359
+ (SELECT SUM(tokens_discovery) FROM observations) as total_discovery_tokens,
360
+ (SELECT SUM(tokens_read) FROM observations) as total_read_tokens,
361
+ (SELECT COUNT(*) FROM observation_embeddings) as total_embeddings
362
+ `)
363
+ };
364
+
365
+ // Database API
366
+ export const database = {
367
+ // Session operations
368
+ createSession(id, projectPath, sessionKey, source = 'unknown') {
369
+ try {
370
+ stmts.createSession.run(id, projectPath, sessionKey, source);
371
+ return { success: true, id };
372
+ } catch (err) {
373
+ // Session might already exist
374
+ return { success: false, error: err.message };
375
+ }
376
+ },
377
+
378
+ getSession(id) {
379
+ return stmts.getSession.get(id);
380
+ },
381
+
382
+ getActiveSession(sessionKey) {
383
+ return stmts.getActiveSession.get(sessionKey);
384
+ },
385
+
386
+ endSession(id) {
387
+ stmts.endSession.run(id);
388
+ },
389
+
390
+ // Observation operations - extended with type, narrative, facts, files tracking
391
+ saveObservation(sessionId, toolName, toolInput, toolResponse, options = {}) {
392
+ const {
393
+ summary = null,
394
+ concepts = null,
395
+ tokensDiscovery = 0,
396
+ tokensRead = 0,
397
+ type = null,
398
+ narrative = null,
399
+ facts = null,
400
+ filesRead = null,
401
+ filesModified = null
402
+ } = options;
403
+
404
+ const result = stmts.saveObservation.run(
405
+ sessionId,
406
+ toolName,
407
+ JSON.stringify(toolInput),
408
+ JSON.stringify(toolResponse),
409
+ summary,
410
+ concepts,
411
+ tokensDiscovery,
412
+ tokensRead,
413
+ type,
414
+ narrative,
415
+ typeof facts === 'string' ? facts : JSON.stringify(facts),
416
+ typeof filesRead === 'string' ? filesRead : JSON.stringify(filesRead),
417
+ typeof filesModified === 'string' ? filesModified : JSON.stringify(filesModified)
418
+ );
419
+
420
+ return { success: true, id: result.lastInsertRowid };
421
+ },
422
+
423
+ getObservation(id) {
424
+ const row = stmts.getObservation.get(id);
425
+ if (row) {
426
+ row.tool_input = JSON.parse(row.tool_input || '{}');
427
+ row.tool_response = JSON.parse(row.tool_response || '{}');
428
+ }
429
+ return row;
430
+ },
431
+
432
+ getObservations(ids) {
433
+ const rows = stmts.getObservations.all(JSON.stringify(ids));
434
+ return rows.map(row => ({
435
+ ...row,
436
+ tool_input: JSON.parse(row.tool_input || '{}'),
437
+ tool_response: JSON.parse(row.tool_response || '{}')
438
+ }));
439
+ },
440
+
441
+ updateObservationSummary(id, summary, concepts, tokensRead) {
442
+ stmts.updateObservationSummary.run(summary, concepts, tokensRead, id);
443
+ },
444
+
445
+ getRecentObservations(projectPath, limit = 50) {
446
+ const rows = projectPath
447
+ ? stmts.getRecentObservations.all(projectPath, limit)
448
+ : stmts.getRecentObservationsAll.all(limit);
449
+
450
+ return rows.map(row => ({
451
+ ...row,
452
+ tool_input: JSON.parse(row.tool_input || '{}'),
453
+ tool_response: JSON.parse(row.tool_response || '{}')
454
+ }));
455
+ },
456
+
457
+ searchObservations(query, limit = 20) {
458
+ try {
459
+ // Check if query contains CJK characters (Chinese/Japanese/Korean)
460
+ const hasCJK = /[\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff]/.test(query);
461
+
462
+ let rows = [];
463
+
464
+ if (!hasCJK) {
465
+ // For non-CJK, use FTS5 search
466
+ // Escape FTS5 special characters by wrapping in double quotes
467
+ const safeQuery = query.includes('.') || query.includes('*') || query.includes('+')
468
+ ? `"${query.replace(/"/g, '""')}"`
469
+ : query;
470
+ rows = stmts.searchObservations.all(safeQuery, limit);
471
+ }
472
+
473
+ // If FTS5 returned no results or query has CJK, use LIKE fallback
474
+ if (rows.length === 0) {
475
+ // Split query by spaces and search for each term (AND logic)
476
+ const terms = query.split(/\s+/).filter(t => t.length > 0);
477
+
478
+ if (terms.length === 1) {
479
+ // Single term - simple LIKE
480
+ const likeQuery = `%${terms[0]}%`;
481
+ rows = db.prepare(`
482
+ SELECT o.*, s.project_path
483
+ FROM observations o
484
+ JOIN sessions s ON o.session_id = s.id
485
+ WHERE o.summary LIKE ?
486
+ OR o.concepts LIKE ?
487
+ OR o.narrative LIKE ?
488
+ OR o.facts LIKE ?
489
+ ORDER BY o.timestamp DESC
490
+ LIMIT ?
491
+ `).all(likeQuery, likeQuery, likeQuery, likeQuery, limit);
492
+ } else {
493
+ // Multiple terms - search for first term, results should contain all terms
494
+ const firstTerm = `%${terms[0]}%`;
495
+ const candidates = db.prepare(`
496
+ SELECT o.*, s.project_path
497
+ FROM observations o
498
+ JOIN sessions s ON o.session_id = s.id
499
+ WHERE o.summary LIKE ?
500
+ OR o.concepts LIKE ?
501
+ OR o.narrative LIKE ?
502
+ OR o.facts LIKE ?
503
+ ORDER BY o.timestamp DESC
504
+ LIMIT 100
505
+ `).all(firstTerm, firstTerm, firstTerm, firstTerm);
506
+
507
+ // Filter to rows containing all terms
508
+ rows = candidates.filter(row => {
509
+ const text = `${row.summary || ''} ${row.concepts || ''} ${row.narrative || ''} ${row.facts || ''}`.toLowerCase();
510
+ return terms.every(term => text.includes(term.toLowerCase()));
511
+ }).slice(0, limit);
512
+ }
513
+ }
514
+
515
+ return rows.map(row => ({
516
+ ...row,
517
+ tool_input: JSON.parse(row.tool_input || '{}'),
518
+ tool_response: JSON.parse(row.tool_response || '{}'),
519
+ facts: row.facts ? JSON.parse(row.facts) : null,
520
+ files_read: row.files_read ? JSON.parse(row.files_read) : null,
521
+ files_modified: row.files_modified ? JSON.parse(row.files_modified) : null
522
+ }));
523
+ } catch (err) {
524
+ console.error('[openclaw-mem] Search error:', err.message);
525
+ return [];
526
+ }
527
+ },
528
+
529
+ getObservationsByType(type, limit = 20) {
530
+ const rows = stmts.getObservationsByType.all(type, limit);
531
+ return rows.map(row => ({
532
+ ...row,
533
+ tool_input: JSON.parse(row.tool_input || '{}'),
534
+ tool_response: JSON.parse(row.tool_response || '{}'),
535
+ facts: row.facts ? JSON.parse(row.facts) : null,
536
+ files_read: row.files_read ? JSON.parse(row.files_read) : null,
537
+ files_modified: row.files_modified ? JSON.parse(row.files_modified) : null
538
+ }));
539
+ },
540
+
541
+ // User prompt operations
542
+ saveUserPrompt(sessionId, content) {
543
+ const result = stmts.saveUserPrompt.run(sessionId, content);
544
+ return { success: true, id: result.lastInsertRowid };
545
+ },
546
+
547
+ getRecentUserPrompts(sessionId, limit = 10) {
548
+ return stmts.getRecentUserPrompts.all(sessionId, limit);
549
+ },
550
+
551
+ // Summary operations
552
+ saveSummary(sessionId, content, request = null, investigated = null, learned = null, completed = null, nextSteps = null) {
553
+ const result = stmts.saveSummary.run(sessionId, content, request, investigated, learned, completed, nextSteps);
554
+ return { success: true, id: result.lastInsertRowid };
555
+ },
556
+
557
+ getRecentSummaries(projectPath, limit = 5) {
558
+ return stmts.getRecentSummaries.all(projectPath, limit);
559
+ },
560
+
561
+ getSummaryBySession(sessionId) {
562
+ return stmts.getSummaryBySession.get(sessionId);
563
+ },
564
+
565
+ getSummaryBySessionKey(sessionKey) {
566
+ return stmts.getSummaryBySessionKey.get(sessionKey);
567
+ },
568
+
569
+ // Embedding operations
570
+ saveEmbedding(observationId, embedding) {
571
+ try {
572
+ // sqlite-vec expects Float32Array directly, not Buffer
573
+ const vec = embedding instanceof Float32Array
574
+ ? embedding
575
+ : new Float32Array(embedding);
576
+ stmts.saveEmbedding.run(BigInt(observationId), vec);
577
+ return { success: true };
578
+ } catch (err) {
579
+ console.error('[openclaw-mem] saveEmbedding error:', err.message);
580
+ return { success: false, error: err.message };
581
+ }
582
+ },
583
+
584
+ searchByVector(embedding, limit = 20) {
585
+ try {
586
+ const vec = embedding instanceof Float32Array
587
+ ? embedding
588
+ : new Float32Array(embedding);
589
+ const rows = stmts.searchByVector.all(vec, limit);
590
+ return rows;
591
+ } catch (err) {
592
+ console.error('[openclaw-mem] searchByVector error:', err.message);
593
+ return [];
594
+ }
595
+ },
596
+
597
+ getEmbeddingCount() {
598
+ try {
599
+ return stmts.getEmbeddingCount.get().count;
600
+ } catch {
601
+ return 0;
602
+ }
603
+ },
604
+
605
+ getObservationsWithoutEmbeddings(limit = 100) {
606
+ try {
607
+ return stmts.getObservationsWithoutEmbeddings.all(limit);
608
+ } catch (err) {
609
+ console.error('[openclaw-mem] getObservationsWithoutEmbeddings error:', err.message);
610
+ return [];
611
+ }
612
+ },
613
+
614
+ // Stats
615
+ getStats() {
616
+ return stmts.getStats.get();
617
+ },
618
+
619
+ // Close database
620
+ close() {
621
+ db.close();
622
+ }
623
+ };
624
+
625
+ export default database;