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