openclaw-mem 1.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.
@@ -0,0 +1,401 @@
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 database search
190
+ */
191
+ function buildTopicSummaries() {
192
+ // Key topics to search for (based on common interests)
193
+ const topics = [
194
+ { name: 'AI记忆系统', keywords: ['短期记忆', '长期记忆', 'AI记忆', '向量数据库', 'RAG'] },
195
+ { name: '长寿与健身', keywords: ['长寿', '禁食', '热量限制', '力量训练', '有氧运动'] },
196
+ { name: '编程技术', keywords: ['Node.js', 'TypeScript', '微服务', '架构'] }
197
+ ];
198
+
199
+ const sections = [];
200
+
201
+ for (const topic of topics) {
202
+ const topicResults = [];
203
+ for (const kw of topic.keywords) {
204
+ try {
205
+ const results = database.searchObservations(kw, 2);
206
+ for (const r of results) {
207
+ // Avoid duplicates
208
+ if (!topicResults.find(t => t.id === r.id)) {
209
+ topicResults.push(r);
210
+ }
211
+ }
212
+ } catch (e) {
213
+ // Search might fail, continue
214
+ }
215
+ }
216
+
217
+ if (topicResults.length > 0) {
218
+ sections.push(`### ${topic.name}`);
219
+ sections.push('');
220
+ for (const r of topicResults.slice(0, 3)) {
221
+ const summary = r.summary || '';
222
+ if (summary.length > 30) {
223
+ sections.push(`- **#${r.id}**: ${summary.slice(0, 150)}${summary.length > 150 ? '...' : ''}`);
224
+ }
225
+ }
226
+ sections.push('');
227
+ }
228
+ }
229
+
230
+ if (sections.length > 0) {
231
+ return '## 历史话题讨论\n\n' + sections.join('\n');
232
+ }
233
+ return '';
234
+ }
235
+
236
+ /**
237
+ * Build complete context for session injection
238
+ */
239
+ export function buildContext(projectPath, options = {}) {
240
+ const {
241
+ observationLimit = 50,
242
+ fullDetailCount = 5,
243
+ showTokenEconomics = true,
244
+ showRetrievalInstructions = true
245
+ } = options;
246
+
247
+ // Fetch recent observations and filter out low-value ones
248
+ const rawObservations = database.getRecentObservations(projectPath, observationLimit * 3); // Fetch more to compensate for filtering
249
+ const observations = filterHighValueObservations(rawObservations).slice(0, observationLimit);
250
+
251
+ if (observations.length === 0) {
252
+ return null; // No context to inject
253
+ }
254
+
255
+ // Fetch recent summaries
256
+ const summaries = database.getRecentSummaries(projectPath, 3);
257
+
258
+ // Build context parts
259
+ const parts = [];
260
+
261
+ // Header
262
+ parts.push('<openclaw-mem-context>');
263
+ parts.push('# Recent Activity');
264
+ parts.push('');
265
+
266
+ // Token economics
267
+ if (showTokenEconomics) {
268
+ parts.push(buildTokenEconomics(observations));
269
+ parts.push('');
270
+ }
271
+
272
+ // Topic summaries (from historical search)
273
+ const topicSummaries = buildTopicSummaries();
274
+ if (topicSummaries) {
275
+ parts.push(topicSummaries);
276
+ parts.push('');
277
+ }
278
+
279
+ // Index table (all observations, compact)
280
+ parts.push('## Index');
281
+ parts.push('');
282
+ parts.push(buildIndexTable(observations));
283
+
284
+ // Full details (top N)
285
+ if (fullDetailCount > 0) {
286
+ const details = buildFullDetails(observations, fullDetailCount);
287
+ if (details) {
288
+ parts.push('## Recent Details');
289
+ parts.push('');
290
+ parts.push(details);
291
+ }
292
+ }
293
+
294
+ // Session summaries
295
+ if (summaries.length > 0) {
296
+ parts.push('## Previous Sessions');
297
+ parts.push('');
298
+ for (const s of summaries) {
299
+ if (s.request) parts.push(`- **Goal**: ${s.request}`);
300
+ if (s.completed) parts.push(`- **Completed**: ${s.completed}`);
301
+ if (s.next_steps) parts.push(`- **Next**: ${s.next_steps}`);
302
+ parts.push('');
303
+ }
304
+ }
305
+
306
+ // Retrieval instructions
307
+ if (showRetrievalInstructions) {
308
+ parts.push(buildRetrievalInstructions());
309
+ }
310
+
311
+ parts.push('</openclaw-mem-context>');
312
+
313
+ return parts.join('\n');
314
+ }
315
+
316
+ /**
317
+ * Search observations and return formatted results
318
+ */
319
+ export function searchContext(query, limit = 20) {
320
+ const results = database.searchObservations(query, limit);
321
+
322
+ if (results.length === 0) {
323
+ return `No observations found for query: "${query}"`;
324
+ }
325
+
326
+ const lines = [
327
+ `## Search Results for "${query}"`,
328
+ '',
329
+ '| ID | Tool | Summary | Date |',
330
+ '|----|------|---------|------|'
331
+ ];
332
+
333
+ for (const r of results) {
334
+ const summary = r.summary_highlight || r.summary || `${r.tool_name} operation`;
335
+ const truncSummary = summary.length > 60 ? summary.slice(0, 57) + '...' : summary;
336
+ const date = formatDate(r.timestamp);
337
+ lines.push(`| #${r.id} | ${r.tool_name} | ${truncSummary} | ${date} |`);
338
+ }
339
+
340
+ lines.push('');
341
+ lines.push(`*${results.length} results. Use \`/memory get <id>\` for full details.*`);
342
+
343
+ return lines.join('\n');
344
+ }
345
+
346
+ /**
347
+ * Get full observation details by IDs
348
+ */
349
+ export function getObservationDetails(ids) {
350
+ const observations = database.getObservations(ids);
351
+
352
+ if (observations.length === 0) {
353
+ return `No observations found for IDs: ${ids.join(', ')}`;
354
+ }
355
+
356
+ return buildFullDetails(observations, observations.length);
357
+ }
358
+
359
+ /**
360
+ * Get timeline around an observation
361
+ */
362
+ export function getTimeline(anchorId, depthBefore = 3, depthAfter = 2) {
363
+ const anchor = database.getObservation(anchorId);
364
+ if (!anchor) {
365
+ return `Observation #${anchorId} not found`;
366
+ }
367
+
368
+ // Get surrounding observations from same session
369
+ const allObs = database.getRecentObservations(null, 100);
370
+ const anchorIdx = allObs.findIndex(o => o.id === anchorId);
371
+
372
+ if (anchorIdx === -1) {
373
+ return buildFullDetails([anchor], 1);
374
+ }
375
+
376
+ const startIdx = Math.max(0, anchorIdx - depthAfter); // Note: list is DESC, so after = before in time
377
+ const endIdx = Math.min(allObs.length, anchorIdx + depthBefore + 1);
378
+ const timeline = allObs.slice(startIdx, endIdx).reverse();
379
+
380
+ const lines = [
381
+ `## Timeline around #${anchorId}`,
382
+ ''
383
+ ];
384
+
385
+ for (const o of timeline) {
386
+ const marker = o.id === anchorId ? '**→**' : ' ';
387
+ const time = formatTime(o.timestamp);
388
+ const emoji = getTypeEmoji(o.tool_name);
389
+ const summary = o.summary || `${o.tool_name} operation`;
390
+ lines.push(`${marker} ${time} ${emoji} #${o.id}: ${summary}`);
391
+ }
392
+
393
+ return lines.join('\n');
394
+ }
395
+
396
+ export default {
397
+ buildContext,
398
+ searchContext,
399
+ getObservationDetails,
400
+ getTimeline
401
+ };
@@ -0,0 +1,309 @@
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;