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/lib/context-builder.js
DELETED
|
@@ -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;
|