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.
Files changed (46) hide show
  1. package/HOOK.md +125 -0
  2. package/LICENSE +1 -1
  3. package/MCP.json +11 -0
  4. package/README.md +146 -168
  5. package/context-builder.js +703 -0
  6. package/database.js +520 -0
  7. package/debug-logger.js +280 -0
  8. package/extractor.js +211 -0
  9. package/gateway-llm.js +155 -0
  10. package/handler.js +1122 -0
  11. package/mcp-http-api.js +356 -0
  12. package/mcp-server.js +525 -0
  13. package/mem-get.sh +24 -0
  14. package/mem-search.sh +17 -0
  15. package/monitor.js +112 -0
  16. package/package.json +53 -29
  17. package/realtime-monitor.js +371 -0
  18. package/session-watcher.js +192 -0
  19. package/setup.js +114 -0
  20. package/sync-recent.js +63 -0
  21. package/README_CN.md +0 -201
  22. package/bin/openclaw-mem.js +0 -117
  23. package/docs/locales/README_AR.md +0 -35
  24. package/docs/locales/README_DE.md +0 -35
  25. package/docs/locales/README_ES.md +0 -35
  26. package/docs/locales/README_FR.md +0 -35
  27. package/docs/locales/README_HE.md +0 -35
  28. package/docs/locales/README_HI.md +0 -35
  29. package/docs/locales/README_ID.md +0 -35
  30. package/docs/locales/README_IT.md +0 -35
  31. package/docs/locales/README_JA.md +0 -57
  32. package/docs/locales/README_KO.md +0 -35
  33. package/docs/locales/README_NL.md +0 -35
  34. package/docs/locales/README_PL.md +0 -35
  35. package/docs/locales/README_PT.md +0 -35
  36. package/docs/locales/README_RU.md +0 -35
  37. package/docs/locales/README_TH.md +0 -35
  38. package/docs/locales/README_TR.md +0 -35
  39. package/docs/locales/README_UK.md +0 -35
  40. package/docs/locales/README_VI.md +0 -35
  41. package/docs/logo.svg +0 -32
  42. package/lib/context-builder.js +0 -415
  43. package/lib/database.js +0 -309
  44. package/lib/handler.js +0 -494
  45. package/scripts/commands.js +0 -141
  46. package/scripts/init.js +0 -248
@@ -1,415 +0,0 @@
1
- /**
2
- * OpenClaw-Mem Context Builder
3
- * Generates context to inject into new sessions using progressive disclosure
4
- */
5
-
6
- import database from './database.js';
7
-
8
- // Token estimation (4 chars ≈ 1 token)
9
- const CHARS_PER_TOKEN = 4;
10
-
11
- function estimateTokens(text) {
12
- if (!text) return 0;
13
- return Math.ceil(String(text).length / CHARS_PER_TOKEN);
14
- }
15
-
16
- // Type emoji mapping
17
- const TYPE_EMOJI = {
18
- 'Edit': '📝',
19
- 'Write': '✏️',
20
- 'Read': '📖',
21
- 'Bash': '💻',
22
- 'Grep': '🔍',
23
- 'Glob': '📁',
24
- 'WebFetch': '🌐',
25
- 'WebSearch': '🔎',
26
- 'Task': '🤖',
27
- 'default': '🔵'
28
- };
29
-
30
- function getTypeEmoji(toolName) {
31
- return TYPE_EMOJI[toolName] || TYPE_EMOJI.default;
32
- }
33
-
34
- // Format timestamp
35
- function formatTime(timestamp) {
36
- if (!timestamp) return '';
37
- const date = new Date(timestamp);
38
- return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
39
- }
40
-
41
- function formatDate(timestamp) {
42
- if (!timestamp) return '';
43
- const date = new Date(timestamp);
44
- return date.toISOString().split('T')[0];
45
- }
46
-
47
- // Filter out low-value observations (like recall test queries)
48
- function filterHighValueObservations(observations) {
49
- const lowValuePatterns = [
50
- '请查看 SESSION-MEMORY',
51
- 'SESSION-MEMORY.md 里没有',
52
- '请查看 SESSION-MEMORY.md,告诉我',
53
- '记忆检索当前不可用',
54
- '/memory search',
55
- '/memory get',
56
- '之前没有记录到',
57
- '没有任何关于'
58
- ];
59
-
60
- return observations.filter(obs => {
61
- const summary = obs.summary || '';
62
- // Always filter out observations matching low-value patterns
63
- const isLowValue = lowValuePatterns.some(pattern => summary.includes(pattern));
64
- if (isLowValue) return false;
65
-
66
- // Keep observations that have actual content (not just metadata)
67
- return true;
68
- });
69
- }
70
-
71
- // Group observations by date
72
- function groupByDate(observations) {
73
- const groups = {};
74
- for (const obs of observations) {
75
- const date = formatDate(obs.timestamp);
76
- if (!groups[date]) {
77
- groups[date] = [];
78
- }
79
- groups[date].push(obs);
80
- }
81
- return groups;
82
- }
83
-
84
- // Build index table (Layer 1 - compact)
85
- function buildIndexTable(observations) {
86
- if (!observations || observations.length === 0) {
87
- return '*(No recent observations)*';
88
- }
89
-
90
- const grouped = groupByDate(observations);
91
- const lines = [];
92
-
93
- for (const [date, obs] of Object.entries(grouped)) {
94
- lines.push(`### ${date}`);
95
- lines.push('');
96
- lines.push('| ID | Time | T | Summary | Tokens |');
97
- lines.push('|----|------|---|---------|--------|');
98
-
99
- for (const o of obs) {
100
- const id = `#${o.id}`;
101
- const time = formatTime(o.timestamp);
102
- const emoji = getTypeEmoji(o.tool_name);
103
- const summary = o.summary || `${o.tool_name} operation`;
104
- const truncSummary = summary.length > 50 ? summary.slice(0, 47) + '...' : summary;
105
- const tokens = `~${o.tokens_read || estimateTokens(summary)}`;
106
-
107
- lines.push(`| ${id} | ${time} | ${emoji} | ${truncSummary} | ${tokens} |`);
108
- }
109
- lines.push('');
110
- }
111
-
112
- return lines.join('\n');
113
- }
114
-
115
- // Build full details (Layer 3 - expensive)
116
- function buildFullDetails(observations, limit = 5) {
117
- if (!observations || observations.length === 0) {
118
- return '';
119
- }
120
-
121
- const toShow = observations.slice(0, limit);
122
- const lines = [];
123
-
124
- for (const o of toShow) {
125
- lines.push(`#### #${o.id} - ${o.tool_name}`);
126
- lines.push('');
127
-
128
- if (o.summary) {
129
- lines.push(`**Summary**: ${o.summary}`);
130
- lines.push('');
131
- }
132
-
133
- // Show key facts from tool input
134
- const input = o.tool_input || {};
135
- const facts = [];
136
-
137
- if (input.file_path) facts.push(`- File: \`${input.file_path}\``);
138
- if (input.command) facts.push(`- Command: \`${input.command.slice(0, 100)}\``);
139
- if (input.pattern) facts.push(`- Pattern: \`${input.pattern}\``);
140
- if (input.query) facts.push(`- Query: ${input.query.slice(0, 100)}`);
141
- if (input.url) facts.push(`- URL: ${input.url}`);
142
-
143
- if (facts.length > 0) {
144
- lines.push('**Details**:');
145
- lines.push(...facts);
146
- lines.push('');
147
- }
148
-
149
- lines.push('---');
150
- lines.push('');
151
- }
152
-
153
- return lines.join('\n');
154
- }
155
-
156
- // Build token economics summary
157
- function buildTokenEconomics(observations) {
158
- let totalDiscovery = 0;
159
- let totalRead = 0;
160
-
161
- for (const o of observations) {
162
- totalDiscovery += o.tokens_discovery || 0;
163
- totalRead += o.tokens_read || estimateTokens(o.summary || '');
164
- }
165
-
166
- const savings = totalDiscovery - totalRead;
167
- const savingsPercent = totalDiscovery > 0 ? Math.round((savings / totalDiscovery) * 100) : 0;
168
-
169
- if (totalDiscovery === 0) {
170
- return `**Observations**: ${observations.length} | **Read cost**: ~${totalRead} tokens`;
171
- }
172
-
173
- return `**Discovery**: ${totalDiscovery} tokens | **Read**: ${totalRead} tokens | **Saved**: ${savings} (${savingsPercent}%)`;
174
- }
175
-
176
- // Build retrieval instructions
177
- function buildRetrievalInstructions() {
178
- return `
179
- ---
180
-
181
- **Need more context?** Use these commands:
182
- - Search: \`/memory search <query>\`
183
- - Get details: \`/memory get <id>\`
184
- - Timeline: \`/memory timeline <id>\`
185
- `;
186
- }
187
-
188
- /**
189
- * Build topic summaries from user's actual recorded concepts
190
- * Dynamically extracts topics from what the user has discussed
191
- */
192
- function buildTopicSummaries() {
193
- // Get all recent observations to extract actual concepts
194
- const recentObs = database.getRecentObservations(null, 100);
195
-
196
- // Extract and count concepts from user's actual data
197
- const conceptCounts = {};
198
- for (const obs of recentObs) {
199
- if (obs.concepts) {
200
- const concepts = obs.concepts.split(',').map(c => c.trim()).filter(c => c.length > 1);
201
- for (const concept of concepts) {
202
- // Skip generic tool names
203
- if (['edit', 'bash', 'read', 'grep', 'write', 'glob'].includes(concept.toLowerCase())) continue;
204
- conceptCounts[concept] = (conceptCounts[concept] || 0) + 1;
205
- }
206
- }
207
- }
208
-
209
- // Get top concepts (mentioned at least twice)
210
- const topConcepts = Object.entries(conceptCounts)
211
- .filter(([_, count]) => count >= 2)
212
- .sort((a, b) => b[1] - a[1])
213
- .slice(0, 10)
214
- .map(([concept, _]) => concept);
215
-
216
- if (topConcepts.length === 0) return '';
217
-
218
- const sections = [];
219
- const seenIds = new Set();
220
-
221
- // Search for each top concept
222
- for (const concept of topConcepts.slice(0, 5)) {
223
- try {
224
- const results = database.searchObservations(concept, 3);
225
- const newResults = results.filter(r => !seenIds.has(r.id));
226
-
227
- if (newResults.length > 0) {
228
- sections.push(`### ${concept}`);
229
- sections.push('');
230
- for (const r of newResults.slice(0, 2)) {
231
- seenIds.add(r.id);
232
- const summary = r.summary || '';
233
- if (summary.length > 20) {
234
- sections.push(`- **#${r.id}**: ${summary.slice(0, 150)}${summary.length > 150 ? '...' : ''}`);
235
- }
236
- }
237
- sections.push('');
238
- }
239
- } catch (e) {
240
- // Search might fail, continue
241
- }
242
- }
243
-
244
- if (sections.length > 0) {
245
- return '## 历史话题讨论\n\n基于您的实际对话自动提取的主题:\n\n' + sections.join('\n');
246
- }
247
- return '';
248
- }
249
-
250
- /**
251
- * Build complete context for session injection
252
- */
253
- export function buildContext(projectPath, options = {}) {
254
- const {
255
- observationLimit = 50,
256
- fullDetailCount = 5,
257
- showTokenEconomics = true,
258
- showRetrievalInstructions = true
259
- } = options;
260
-
261
- // Fetch recent observations and filter out low-value ones
262
- const rawObservations = database.getRecentObservations(projectPath, observationLimit * 3); // Fetch more to compensate for filtering
263
- const observations = filterHighValueObservations(rawObservations).slice(0, observationLimit);
264
-
265
- if (observations.length === 0) {
266
- return null; // No context to inject
267
- }
268
-
269
- // Fetch recent summaries
270
- const summaries = database.getRecentSummaries(projectPath, 3);
271
-
272
- // Build context parts
273
- const parts = [];
274
-
275
- // Header
276
- parts.push('<openclaw-mem-context>');
277
- parts.push('# Recent Activity');
278
- parts.push('');
279
-
280
- // Token economics
281
- if (showTokenEconomics) {
282
- parts.push(buildTokenEconomics(observations));
283
- parts.push('');
284
- }
285
-
286
- // Topic summaries (from historical search)
287
- const topicSummaries = buildTopicSummaries();
288
- if (topicSummaries) {
289
- parts.push(topicSummaries);
290
- parts.push('');
291
- }
292
-
293
- // Index table (all observations, compact)
294
- parts.push('## Index');
295
- parts.push('');
296
- parts.push(buildIndexTable(observations));
297
-
298
- // Full details (top N)
299
- if (fullDetailCount > 0) {
300
- const details = buildFullDetails(observations, fullDetailCount);
301
- if (details) {
302
- parts.push('## Recent Details');
303
- parts.push('');
304
- parts.push(details);
305
- }
306
- }
307
-
308
- // Session summaries
309
- if (summaries.length > 0) {
310
- parts.push('## Previous Sessions');
311
- parts.push('');
312
- for (const s of summaries) {
313
- if (s.request) parts.push(`- **Goal**: ${s.request}`);
314
- if (s.completed) parts.push(`- **Completed**: ${s.completed}`);
315
- if (s.next_steps) parts.push(`- **Next**: ${s.next_steps}`);
316
- parts.push('');
317
- }
318
- }
319
-
320
- // Retrieval instructions
321
- if (showRetrievalInstructions) {
322
- parts.push(buildRetrievalInstructions());
323
- }
324
-
325
- parts.push('</openclaw-mem-context>');
326
-
327
- return parts.join('\n');
328
- }
329
-
330
- /**
331
- * Search observations and return formatted results
332
- */
333
- export function searchContext(query, limit = 20) {
334
- const results = database.searchObservations(query, limit);
335
-
336
- if (results.length === 0) {
337
- return `No observations found for query: "${query}"`;
338
- }
339
-
340
- const lines = [
341
- `## Search Results for "${query}"`,
342
- '',
343
- '| ID | Tool | Summary | Date |',
344
- '|----|------|---------|------|'
345
- ];
346
-
347
- for (const r of results) {
348
- const summary = r.summary_highlight || r.summary || `${r.tool_name} operation`;
349
- const truncSummary = summary.length > 60 ? summary.slice(0, 57) + '...' : summary;
350
- const date = formatDate(r.timestamp);
351
- lines.push(`| #${r.id} | ${r.tool_name} | ${truncSummary} | ${date} |`);
352
- }
353
-
354
- lines.push('');
355
- lines.push(`*${results.length} results. Use \`/memory get <id>\` for full details.*`);
356
-
357
- return lines.join('\n');
358
- }
359
-
360
- /**
361
- * Get full observation details by IDs
362
- */
363
- export function getObservationDetails(ids) {
364
- const observations = database.getObservations(ids);
365
-
366
- if (observations.length === 0) {
367
- return `No observations found for IDs: ${ids.join(', ')}`;
368
- }
369
-
370
- return buildFullDetails(observations, observations.length);
371
- }
372
-
373
- /**
374
- * Get timeline around an observation
375
- */
376
- export function getTimeline(anchorId, depthBefore = 3, depthAfter = 2) {
377
- const anchor = database.getObservation(anchorId);
378
- if (!anchor) {
379
- return `Observation #${anchorId} not found`;
380
- }
381
-
382
- // Get surrounding observations from same session
383
- const allObs = database.getRecentObservations(null, 100);
384
- const anchorIdx = allObs.findIndex(o => o.id === anchorId);
385
-
386
- if (anchorIdx === -1) {
387
- return buildFullDetails([anchor], 1);
388
- }
389
-
390
- const startIdx = Math.max(0, anchorIdx - depthAfter); // Note: list is DESC, so after = before in time
391
- const endIdx = Math.min(allObs.length, anchorIdx + depthBefore + 1);
392
- const timeline = allObs.slice(startIdx, endIdx).reverse();
393
-
394
- const lines = [
395
- `## Timeline around #${anchorId}`,
396
- ''
397
- ];
398
-
399
- for (const o of timeline) {
400
- const marker = o.id === anchorId ? '**→**' : ' ';
401
- const time = formatTime(o.timestamp);
402
- const emoji = getTypeEmoji(o.tool_name);
403
- const summary = o.summary || `${o.tool_name} operation`;
404
- lines.push(`${marker} ${time} ${emoji} #${o.id}: ${summary}`);
405
- }
406
-
407
- return lines.join('\n');
408
- }
409
-
410
- export default {
411
- buildContext,
412
- searchContext,
413
- getObservationDetails,
414
- getTimeline
415
- };
package/lib/database.js DELETED
@@ -1,309 +0,0 @@
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
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)
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
- -- Summaries table
52
- CREATE TABLE IF NOT EXISTS summaries (
53
- id INTEGER PRIMARY KEY AUTOINCREMENT,
54
- session_id TEXT,
55
- content TEXT,
56
- request TEXT,
57
- completed TEXT,
58
- next_steps TEXT,
59
- created_at TEXT DEFAULT (datetime('now')),
60
- FOREIGN KEY (session_id) REFERENCES sessions(id)
61
- );
62
-
63
- -- Full-text search index
64
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
65
- tool_name,
66
- summary,
67
- concepts,
68
- content='observations',
69
- content_rowid='id'
70
- );
71
-
72
- -- Triggers for FTS sync
73
- CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
74
- INSERT INTO observations_fts(rowid, tool_name, summary, concepts)
75
- VALUES (new.id, new.tool_name, new.summary, new.concepts);
76
- END;
77
-
78
- CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
79
- INSERT INTO observations_fts(observations_fts, rowid, tool_name, summary, concepts)
80
- VALUES ('delete', old.id, old.tool_name, old.summary, old.concepts);
81
- END;
82
-
83
- CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
84
- INSERT INTO observations_fts(observations_fts, rowid, tool_name, summary, concepts)
85
- VALUES ('delete', old.id, old.tool_name, old.summary, old.concepts);
86
- INSERT INTO observations_fts(rowid, tool_name, summary, concepts)
87
- VALUES (new.id, new.tool_name, new.summary, new.concepts);
88
- END;
89
-
90
- -- Indexes
91
- CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
92
- CREATE INDEX IF NOT EXISTS idx_observations_timestamp ON observations(timestamp DESC);
93
- CREATE INDEX IF NOT EXISTS idx_observations_tool ON observations(tool_name);
94
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
95
- `);
96
-
97
- // Prepared statements
98
- const stmts = {
99
- // Sessions
100
- createSession: db.prepare(`
101
- INSERT INTO sessions (id, project_path, session_key, source)
102
- VALUES (?, ?, ?, ?)
103
- `),
104
-
105
- getSession: db.prepare(`
106
- SELECT * FROM sessions WHERE id = ?
107
- `),
108
-
109
- endSession: db.prepare(`
110
- UPDATE sessions SET ended_at = datetime('now'), status = 'completed'
111
- WHERE id = ?
112
- `),
113
-
114
- getActiveSession: db.prepare(`
115
- SELECT * FROM sessions WHERE session_key = ? AND status = 'active'
116
- ORDER BY started_at DESC LIMIT 1
117
- `),
118
-
119
- // Observations
120
- saveObservation: db.prepare(`
121
- INSERT INTO observations (session_id, tool_name, tool_input, tool_response, summary, concepts, tokens_discovery, tokens_read)
122
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
123
- `),
124
-
125
- getObservation: db.prepare(`
126
- SELECT * FROM observations WHERE id = ?
127
- `),
128
-
129
- getObservations: db.prepare(`
130
- SELECT * FROM observations WHERE id IN (SELECT value FROM json_each(?))
131
- `),
132
-
133
- updateObservationSummary: db.prepare(`
134
- UPDATE observations SET summary = ?, concepts = ?, tokens_read = ?
135
- WHERE id = ?
136
- `),
137
-
138
- getRecentObservations: db.prepare(`
139
- SELECT o.*, s.project_path
140
- FROM observations o
141
- JOIN sessions s ON o.session_id = s.id
142
- WHERE s.project_path = ?
143
- ORDER BY o.timestamp DESC
144
- LIMIT ?
145
- `),
146
-
147
- getRecentObservationsAll: db.prepare(`
148
- SELECT o.*, s.project_path
149
- FROM observations o
150
- JOIN sessions s ON o.session_id = s.id
151
- ORDER BY o.timestamp DESC
152
- LIMIT ?
153
- `),
154
-
155
- searchObservations: db.prepare(`
156
- SELECT o.*, s.project_path,
157
- highlight(observations_fts, 1, '<mark>', '</mark>') as summary_highlight
158
- FROM observations_fts fts
159
- JOIN observations o ON fts.rowid = o.id
160
- JOIN sessions s ON o.session_id = s.id
161
- WHERE observations_fts MATCH ?
162
- ORDER BY rank
163
- LIMIT ?
164
- `),
165
-
166
- // Summaries
167
- saveSummary: db.prepare(`
168
- INSERT INTO summaries (session_id, content, request, completed, next_steps)
169
- VALUES (?, ?, ?, ?, ?)
170
- `),
171
-
172
- getRecentSummaries: db.prepare(`
173
- SELECT su.*, s.project_path
174
- FROM summaries su
175
- JOIN sessions s ON su.session_id = s.id
176
- WHERE s.project_path = ?
177
- ORDER BY su.created_at DESC
178
- LIMIT ?
179
- `),
180
-
181
- // Stats
182
- getStats: db.prepare(`
183
- SELECT
184
- (SELECT COUNT(*) FROM sessions) as total_sessions,
185
- (SELECT COUNT(*) FROM observations) as total_observations,
186
- (SELECT COUNT(*) FROM summaries) as total_summaries,
187
- (SELECT SUM(tokens_discovery) FROM observations) as total_discovery_tokens,
188
- (SELECT SUM(tokens_read) FROM observations) as total_read_tokens
189
- `)
190
- };
191
-
192
- // Database API
193
- export const database = {
194
- // Session operations
195
- createSession(id, projectPath, sessionKey, source = 'unknown') {
196
- try {
197
- stmts.createSession.run(id, projectPath, sessionKey, source);
198
- return { success: true, id };
199
- } catch (err) {
200
- // Session might already exist
201
- return { success: false, error: err.message };
202
- }
203
- },
204
-
205
- getSession(id) {
206
- return stmts.getSession.get(id);
207
- },
208
-
209
- getActiveSession(sessionKey) {
210
- return stmts.getActiveSession.get(sessionKey);
211
- },
212
-
213
- endSession(id) {
214
- stmts.endSession.run(id);
215
- },
216
-
217
- // Observation operations
218
- saveObservation(sessionId, toolName, toolInput, toolResponse, options = {}) {
219
- const {
220
- summary = null,
221
- concepts = null,
222
- tokensDiscovery = 0,
223
- tokensRead = 0
224
- } = options;
225
-
226
- const result = stmts.saveObservation.run(
227
- sessionId,
228
- toolName,
229
- JSON.stringify(toolInput),
230
- JSON.stringify(toolResponse),
231
- summary,
232
- concepts,
233
- tokensDiscovery,
234
- tokensRead
235
- );
236
-
237
- return { success: true, id: result.lastInsertRowid };
238
- },
239
-
240
- getObservation(id) {
241
- const row = stmts.getObservation.get(id);
242
- if (row) {
243
- row.tool_input = JSON.parse(row.tool_input || '{}');
244
- row.tool_response = JSON.parse(row.tool_response || '{}');
245
- }
246
- return row;
247
- },
248
-
249
- getObservations(ids) {
250
- const rows = stmts.getObservations.all(JSON.stringify(ids));
251
- return rows.map(row => ({
252
- ...row,
253
- tool_input: JSON.parse(row.tool_input || '{}'),
254
- tool_response: JSON.parse(row.tool_response || '{}')
255
- }));
256
- },
257
-
258
- updateObservationSummary(id, summary, concepts, tokensRead) {
259
- stmts.updateObservationSummary.run(summary, concepts, tokensRead, id);
260
- },
261
-
262
- getRecentObservations(projectPath, limit = 50) {
263
- const rows = projectPath
264
- ? stmts.getRecentObservations.all(projectPath, limit)
265
- : stmts.getRecentObservationsAll.all(limit);
266
-
267
- return rows.map(row => ({
268
- ...row,
269
- tool_input: JSON.parse(row.tool_input || '{}'),
270
- tool_response: JSON.parse(row.tool_response || '{}')
271
- }));
272
- },
273
-
274
- searchObservations(query, limit = 20) {
275
- try {
276
- const rows = stmts.searchObservations.all(query, limit);
277
- return rows.map(row => ({
278
- ...row,
279
- tool_input: JSON.parse(row.tool_input || '{}'),
280
- tool_response: JSON.parse(row.tool_response || '{}')
281
- }));
282
- } catch (err) {
283
- console.error('[openclaw-mem] Search error:', err.message);
284
- return [];
285
- }
286
- },
287
-
288
- // Summary operations
289
- saveSummary(sessionId, content, request = null, completed = null, nextSteps = null) {
290
- const result = stmts.saveSummary.run(sessionId, content, request, completed, nextSteps);
291
- return { success: true, id: result.lastInsertRowid };
292
- },
293
-
294
- getRecentSummaries(projectPath, limit = 5) {
295
- return stmts.getRecentSummaries.all(projectPath, limit);
296
- },
297
-
298
- // Stats
299
- getStats() {
300
- return stmts.getStats.get();
301
- },
302
-
303
- // Close database
304
- close() {
305
- db.close();
306
- }
307
- };
308
-
309
- export default database;