openclaw-mem 1.0.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HOOK.md +125 -0
- package/LICENSE +1 -1
- package/MCP.json +11 -0
- package/README.md +158 -167
- package/backfill-embeddings.js +79 -0
- package/context-builder.js +703 -0
- package/database.js +625 -0
- package/debug-logger.js +280 -0
- package/extractor.js +268 -0
- package/gateway-llm.js +250 -0
- package/handler.js +941 -0
- package/mcp-http-api.js +424 -0
- package/mcp-server.js +605 -0
- package/mem-get.sh +24 -0
- package/mem-search.sh +17 -0
- package/monitor.js +112 -0
- package/package.json +58 -30
- 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/debug-logger.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw-Mem 调试日志工具
|
|
4
|
+
*
|
|
5
|
+
* 显示完整的 API 调用流程:
|
|
6
|
+
* 1. 用户消息
|
|
7
|
+
* 2. AI 调用的 exec 命令
|
|
8
|
+
* 3. API 收到的请求
|
|
9
|
+
* 4. API 返回的响应
|
|
10
|
+
* 5. AI 最终回复
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import http from 'http';
|
|
17
|
+
|
|
18
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
|
|
19
|
+
const LOG_FILE = '/tmp/openclaw-debug.log';
|
|
20
|
+
|
|
21
|
+
// 颜色
|
|
22
|
+
const c = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bold: '\x1b[1m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
cyan: '\x1b[36m',
|
|
32
|
+
bgRed: '\x1b[41m',
|
|
33
|
+
bgGreen: '\x1b[42m',
|
|
34
|
+
bgYellow: '\x1b[43m',
|
|
35
|
+
bgBlue: '\x1b[44m',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function log(msg) {
|
|
39
|
+
const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
40
|
+
const line = `[${ts}] ${msg}`;
|
|
41
|
+
console.log(line);
|
|
42
|
+
fs.appendFileSync(LOG_FILE, line.replace(/\x1b\[[0-9;]*m/g, '') + '\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function logSection(title, color = 'cyan') {
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(`${c[color]}${'═'.repeat(60)}${c.reset}`);
|
|
48
|
+
console.log(`${c[color]}${c.bold} ${title}${c.reset}`);
|
|
49
|
+
console.log(`${c[color]}${'═'.repeat(60)}${c.reset}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logEvent(icon, label, content, color = 'white') {
|
|
53
|
+
const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(`${c.dim}[${ts}]${c.reset} ${icon} ${c[color]}${c.bold}${label}${c.reset}`);
|
|
56
|
+
if (content) {
|
|
57
|
+
const lines = String(content).split('\n').slice(0, 15);
|
|
58
|
+
lines.forEach(line => {
|
|
59
|
+
console.log(` ${c.dim}│${c.reset} ${line.slice(0, 120)}`);
|
|
60
|
+
});
|
|
61
|
+
if (String(content).split('\n').length > 15) {
|
|
62
|
+
console.log(` ${c.dim}│ ... (更多内容省略)${c.reset}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fs.appendFileSync(LOG_FILE, `[${ts}] ${icon} ${label}\n${content || ''}\n\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 监控 Session 文件
|
|
69
|
+
let lastSessionFile = null;
|
|
70
|
+
let processedLines = new Set();
|
|
71
|
+
|
|
72
|
+
function findLatestSession() {
|
|
73
|
+
try {
|
|
74
|
+
const files = fs.readdirSync(SESSIONS_DIR)
|
|
75
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
76
|
+
.map(f => ({
|
|
77
|
+
name: f,
|
|
78
|
+
path: path.join(SESSIONS_DIR, f),
|
|
79
|
+
mtime: fs.statSync(path.join(SESSIONS_DIR, f)).mtime
|
|
80
|
+
}))
|
|
81
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
82
|
+
return files[0]?.path || null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function watchSession() {
|
|
89
|
+
const latestSession = findLatestSession();
|
|
90
|
+
|
|
91
|
+
if (latestSession !== lastSessionFile) {
|
|
92
|
+
lastSessionFile = latestSession;
|
|
93
|
+
processedLines.clear();
|
|
94
|
+
if (latestSession) {
|
|
95
|
+
logEvent('📁', '监控会话文件', path.basename(latestSession), 'cyan');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!lastSessionFile) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(lastSessionFile, 'utf-8');
|
|
103
|
+
const lines = content.trim().split('\n');
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
const lineKey = line.slice(0, 150);
|
|
107
|
+
if (processedLines.has(lineKey)) continue;
|
|
108
|
+
processedLines.add(lineKey);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const entry = JSON.parse(line);
|
|
112
|
+
if (entry.type !== 'message') continue;
|
|
113
|
+
|
|
114
|
+
const msg = entry.message;
|
|
115
|
+
if (!msg) continue;
|
|
116
|
+
|
|
117
|
+
// 用户消息
|
|
118
|
+
if (msg.role === 'user') {
|
|
119
|
+
let text = '';
|
|
120
|
+
if (Array.isArray(msg.content)) {
|
|
121
|
+
const textPart = msg.content.find(c => c.type === 'text');
|
|
122
|
+
text = textPart?.text || '';
|
|
123
|
+
} else {
|
|
124
|
+
text = msg.content || '';
|
|
125
|
+
}
|
|
126
|
+
if (text && !text.startsWith('A new session')) {
|
|
127
|
+
logEvent('👤', '用户消息', text.slice(0, 300), 'green');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// AI 响应
|
|
132
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
133
|
+
// 检查工具调用
|
|
134
|
+
if (Array.isArray(msg.content)) {
|
|
135
|
+
for (const block of msg.content) {
|
|
136
|
+
if (block.type === 'toolCall') {
|
|
137
|
+
const toolName = block.name;
|
|
138
|
+
const args = block.arguments;
|
|
139
|
+
|
|
140
|
+
if (toolName === 'exec') {
|
|
141
|
+
let argsObj;
|
|
142
|
+
try {
|
|
143
|
+
argsObj = typeof args === 'string' ? JSON.parse(args) : args;
|
|
144
|
+
} catch {
|
|
145
|
+
argsObj = { command: args };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
logEvent('🔧', `工具调用: ${toolName}`,
|
|
149
|
+
`命令: ${argsObj.command || JSON.stringify(argsObj)}`, 'yellow');
|
|
150
|
+
|
|
151
|
+
// 高亮显示 curl 命令
|
|
152
|
+
if (argsObj.command && argsObj.command.includes('curl')) {
|
|
153
|
+
console.log(` ${c.bgYellow}${c.bold} CURL 命令 ${c.reset}`);
|
|
154
|
+
console.log(` ${c.yellow}${argsObj.command}${c.reset}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (block.type === 'text' && block.text) {
|
|
160
|
+
logEvent('🤖', 'AI 响应', block.text.slice(0, 400), 'blue');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 工具结果
|
|
167
|
+
if (msg.role === 'toolResult') {
|
|
168
|
+
const toolName = msg.toolName || 'unknown';
|
|
169
|
+
let result = '';
|
|
170
|
+
if (Array.isArray(msg.content)) {
|
|
171
|
+
const textPart = msg.content.find(c => c.type === 'text');
|
|
172
|
+
result = textPart?.text || '';
|
|
173
|
+
} else {
|
|
174
|
+
result = msg.content || '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (toolName === 'exec') {
|
|
178
|
+
const isEmpty = !result || result === '(no output)';
|
|
179
|
+
const color = isEmpty ? 'red' : 'green';
|
|
180
|
+
const icon = isEmpty ? '❌' : '✅';
|
|
181
|
+
logEvent(icon, `exec 返回结果`, result || '(空)', color);
|
|
182
|
+
|
|
183
|
+
if (isEmpty) {
|
|
184
|
+
console.log(` ${c.bgRed}${c.bold} 警告: exec 返回空!curl 可能失败了 ${c.reset}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// 解析失败,忽略
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// 文件读取失败
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 创建代理 API 服务器来拦截请求
|
|
199
|
+
const PROXY_PORT = 18791;
|
|
200
|
+
const TARGET_PORT = 18790;
|
|
201
|
+
|
|
202
|
+
function startProxyServer() {
|
|
203
|
+
const proxy = http.createServer((req, res) => {
|
|
204
|
+
let body = '';
|
|
205
|
+
req.on('data', chunk => body += chunk);
|
|
206
|
+
req.on('end', () => {
|
|
207
|
+
// 记录请求
|
|
208
|
+
logEvent('📥', `API 请求: ${req.method} ${req.url}`,
|
|
209
|
+
body ? `Body: ${body}` : '(无 body)', 'magenta');
|
|
210
|
+
|
|
211
|
+
// 转发到真实 API
|
|
212
|
+
const options = {
|
|
213
|
+
hostname: '127.0.0.1',
|
|
214
|
+
port: TARGET_PORT,
|
|
215
|
+
path: req.url,
|
|
216
|
+
method: req.method,
|
|
217
|
+
headers: req.headers
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const proxyReq = http.request(options, (proxyRes) => {
|
|
221
|
+
let responseBody = '';
|
|
222
|
+
proxyRes.on('data', chunk => responseBody += chunk);
|
|
223
|
+
proxyRes.on('end', () => {
|
|
224
|
+
// 记录响应
|
|
225
|
+
const preview = responseBody.slice(0, 500);
|
|
226
|
+
logEvent('📤', `API 响应 (${proxyRes.statusCode})`, preview,
|
|
227
|
+
proxyRes.statusCode === 200 ? 'green' : 'red');
|
|
228
|
+
|
|
229
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
230
|
+
res.end(responseBody);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
proxyReq.on('error', (err) => {
|
|
235
|
+
logEvent('❌', 'API 请求失败', err.message, 'red');
|
|
236
|
+
res.writeHead(500);
|
|
237
|
+
res.end('Proxy error');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (body) proxyReq.write(body);
|
|
241
|
+
proxyReq.end();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
proxy.listen(PROXY_PORT, '127.0.0.1', () => {
|
|
246
|
+
log(`${c.green}代理服务器运行在 http://127.0.0.1:${PROXY_PORT}${c.reset}`);
|
|
247
|
+
log(`${c.dim}(转发到 http://127.0.0.1:${TARGET_PORT})${c.reset}`);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return proxy;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 主函数
|
|
254
|
+
function main() {
|
|
255
|
+
// 清空日志文件
|
|
256
|
+
fs.writeFileSync(LOG_FILE, '');
|
|
257
|
+
|
|
258
|
+
logSection('OpenClaw-Mem 调试日志工具', 'cyan');
|
|
259
|
+
log('');
|
|
260
|
+
log(`${c.bold}监控内容:${c.reset}`);
|
|
261
|
+
log(` • Session 文件: ${SESSIONS_DIR}`);
|
|
262
|
+
log(` • 日志文件: ${LOG_FILE}`);
|
|
263
|
+
log('');
|
|
264
|
+
log(`${c.yellow}提示: 按 Ctrl+C 退出${c.reset}`);
|
|
265
|
+
log('');
|
|
266
|
+
|
|
267
|
+
// 开始监控
|
|
268
|
+
setInterval(watchSession, 300);
|
|
269
|
+
|
|
270
|
+
logSection('等待事件...', 'dim');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main();
|
|
274
|
+
|
|
275
|
+
process.on('SIGINT', () => {
|
|
276
|
+
console.log();
|
|
277
|
+
log(`${c.yellow}调试日志已停止${c.reset}`);
|
|
278
|
+
log(`${c.dim}完整日志保存在: ${LOG_FILE}${c.reset}`);
|
|
279
|
+
process.exit(0);
|
|
280
|
+
});
|
package/extractor.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw-Mem LLM Extractor
|
|
3
|
+
*
|
|
4
|
+
* Structured observation extraction inspired by claude-mem's observer agent pattern.
|
|
5
|
+
* Uses DeepSeek API to produce rich, searchable memory records.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { callGatewayChat } from './gateway-llm.js';
|
|
9
|
+
|
|
10
|
+
// ── Valid concept categories (fixed taxonomy for consistent search) ──
|
|
11
|
+
const VALID_CONCEPTS = [
|
|
12
|
+
'how-it-works', // understanding mechanisms
|
|
13
|
+
'why-it-exists', // purpose or rationale
|
|
14
|
+
'what-changed', // modifications made
|
|
15
|
+
'problem-solution', // issues and their fixes
|
|
16
|
+
'gotcha', // traps or edge cases
|
|
17
|
+
'pattern', // reusable approach
|
|
18
|
+
'trade-off' // pros/cons of a decision
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// ── Cache ──
|
|
22
|
+
const conceptCache = new Map();
|
|
23
|
+
const CACHE_MAX_SIZE = 1000;
|
|
24
|
+
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
25
|
+
|
|
26
|
+
function getCacheKey(text) {
|
|
27
|
+
let hash = 0;
|
|
28
|
+
const str = text.slice(0, 500);
|
|
29
|
+
for (let i = 0; i < str.length; i++) {
|
|
30
|
+
const char = str.charCodeAt(i);
|
|
31
|
+
hash = ((hash << 5) - hash) + char;
|
|
32
|
+
hash = hash & hash;
|
|
33
|
+
}
|
|
34
|
+
return hash.toString(16);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanCache() {
|
|
38
|
+
if (conceptCache.size > CACHE_MAX_SIZE) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
for (const [key, value] of conceptCache.entries()) {
|
|
41
|
+
if (now - value.timestamp > CACHE_TTL) {
|
|
42
|
+
conceptCache.delete(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (conceptCache.size > CACHE_MAX_SIZE) {
|
|
46
|
+
const entries = [...conceptCache.entries()];
|
|
47
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
48
|
+
const toRemove = entries.slice(0, entries.length - CACHE_MAX_SIZE / 2);
|
|
49
|
+
for (const [key] of toRemove) {
|
|
50
|
+
conceptCache.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract concepts from text using LLM
|
|
58
|
+
*/
|
|
59
|
+
export async function extractConcepts(text, options = {}) {
|
|
60
|
+
if (!text || typeof text !== 'string' || text.trim().length < 10) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cacheKey = getCacheKey(text);
|
|
65
|
+
const cached = conceptCache.get(cacheKey);
|
|
66
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
67
|
+
return cached.concepts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const content = await callGatewayChat([
|
|
72
|
+
{
|
|
73
|
+
role: 'system',
|
|
74
|
+
content: `You are a knowledge classifier. Categorize the given text into 2-4 concept categories from this fixed list:
|
|
75
|
+
- how-it-works: understanding mechanisms or implementation details
|
|
76
|
+
- why-it-exists: purpose, rationale, or motivation
|
|
77
|
+
- what-changed: modifications, updates, or configuration changes
|
|
78
|
+
- problem-solution: issues encountered and their fixes
|
|
79
|
+
- gotcha: traps, edge cases, or surprising behavior
|
|
80
|
+
- pattern: reusable approaches or best practices
|
|
81
|
+
- trade-off: pros/cons analysis or design decisions
|
|
82
|
+
|
|
83
|
+
Return ONLY a JSON array of matching categories. No explanation.`
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: text.slice(0, 2000)
|
|
88
|
+
}
|
|
89
|
+
], { sessionKey: 'extract-concepts', temperature: 0.1, max_tokens: 100 });
|
|
90
|
+
|
|
91
|
+
if (!content) return [];
|
|
92
|
+
|
|
93
|
+
let concepts = [];
|
|
94
|
+
try {
|
|
95
|
+
const match = content.match(/\[[\s\S]*?\]/);
|
|
96
|
+
if (match) {
|
|
97
|
+
concepts = JSON.parse(match[0]);
|
|
98
|
+
}
|
|
99
|
+
} catch (parseErr) {
|
|
100
|
+
console.error('[openclaw-mem] Failed to parse concepts response:', parseErr.message);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate against fixed taxonomy
|
|
105
|
+
concepts = concepts
|
|
106
|
+
.filter(c => typeof c === 'string')
|
|
107
|
+
.map(c => c.trim().toLowerCase())
|
|
108
|
+
.filter(c => VALID_CONCEPTS.includes(c))
|
|
109
|
+
.slice(0, 4);
|
|
110
|
+
|
|
111
|
+
cleanCache();
|
|
112
|
+
conceptCache.set(cacheKey, { concepts, timestamp: Date.now() });
|
|
113
|
+
return concepts;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('[openclaw-mem] Concept extraction error:', err.message);
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract structured observation from a tool call
|
|
122
|
+
*
|
|
123
|
+
* Produces rich, searchable records with:
|
|
124
|
+
* - Accurate type classification
|
|
125
|
+
* - Descriptive title (short, action-oriented)
|
|
126
|
+
* - Detailed narrative (what happened, how it works, why it matters)
|
|
127
|
+
* - Structured facts (self-contained, grep-friendly)
|
|
128
|
+
* - Fixed concept categories
|
|
129
|
+
*/
|
|
130
|
+
export async function extractFromToolCall(data) {
|
|
131
|
+
const { tool_name, tool_input, tool_response, filesRead, filesModified } = data;
|
|
132
|
+
|
|
133
|
+
// Provide generous context (2000 chars each, not 300)
|
|
134
|
+
const inputStr = typeof tool_input === 'string'
|
|
135
|
+
? tool_input.slice(0, 2000)
|
|
136
|
+
: JSON.stringify(tool_input, null, 0).slice(0, 2000);
|
|
137
|
+
|
|
138
|
+
const responseStr = typeof tool_response === 'string'
|
|
139
|
+
? tool_response.slice(0, 2000)
|
|
140
|
+
: JSON.stringify(tool_response, null, 0).slice(0, 2000);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const content = await callGatewayChat([
|
|
144
|
+
{
|
|
145
|
+
role: 'system',
|
|
146
|
+
content: `You are OpenClaw-Mem, a specialized observer that creates searchable memory records for FUTURE SESSIONS.
|
|
147
|
+
|
|
148
|
+
Your job: analyze a tool call and produce a structured observation capturing what was LEARNED, BUILT, FIXED, or CONFIGURED.
|
|
149
|
+
|
|
150
|
+
RULES:
|
|
151
|
+
- Record deliverables and capabilities, not process steps
|
|
152
|
+
- Use action verbs: implemented, fixed, deployed, configured, migrated, optimized, discovered, decided
|
|
153
|
+
- The "narrative" field is the most important: explain WHAT happened, HOW it works, and WHY it matters
|
|
154
|
+
- Facts must be self-contained statements (each fact should make sense without the others)
|
|
155
|
+
- Title should be a short noun phrase (3-10 words) capturing the core topic
|
|
156
|
+
|
|
157
|
+
TYPE DEFINITIONS (pick exactly one):
|
|
158
|
+
- bugfix: something was broken and is now fixed
|
|
159
|
+
- feature: new capability or functionality added
|
|
160
|
+
- refactor: code restructured without behavior change
|
|
161
|
+
- change: generic modification (docs, config, dependencies)
|
|
162
|
+
- discovery: learning about existing system, reading code, exploring
|
|
163
|
+
- decision: architectural or design choice with rationale
|
|
164
|
+
|
|
165
|
+
CONCEPT CATEGORIES (pick 1-3):
|
|
166
|
+
- how-it-works: understanding mechanisms
|
|
167
|
+
- why-it-exists: purpose or rationale
|
|
168
|
+
- what-changed: modifications made
|
|
169
|
+
- problem-solution: issues and their fixes
|
|
170
|
+
- gotcha: traps or edge cases
|
|
171
|
+
- pattern: reusable approach
|
|
172
|
+
- trade-off: pros/cons of a decision
|
|
173
|
+
|
|
174
|
+
Return ONLY valid JSON, no markdown fences, no explanation.`
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: `Tool: ${tool_name}
|
|
179
|
+
Input: ${inputStr}
|
|
180
|
+
Output: ${responseStr}
|
|
181
|
+
Files read: ${filesRead?.join(', ') || 'none'}
|
|
182
|
+
Files modified: ${filesModified?.join(', ') || 'none'}
|
|
183
|
+
|
|
184
|
+
Return JSON:
|
|
185
|
+
{
|
|
186
|
+
"type": "one of: bugfix|feature|refactor|change|discovery|decision",
|
|
187
|
+
"title": "Short descriptive title (3-10 words)",
|
|
188
|
+
"narrative": "2-4 sentences: what was done, how it works, why it matters. Be specific and include key details.",
|
|
189
|
+
"facts": ["Self-contained fact 1", "Self-contained fact 2", "...up to 5"],
|
|
190
|
+
"concepts": ["category1", "category2"]
|
|
191
|
+
}`
|
|
192
|
+
}
|
|
193
|
+
], { sessionKey: 'extract-toolcall', temperature: 0.2, max_tokens: 800 });
|
|
194
|
+
|
|
195
|
+
if (!content) throw new Error('empty response');
|
|
196
|
+
|
|
197
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
198
|
+
if (match) {
|
|
199
|
+
const result = JSON.parse(match[0]);
|
|
200
|
+
|
|
201
|
+
// Validate type
|
|
202
|
+
const validTypes = ['bugfix', 'feature', 'refactor', 'change', 'discovery', 'decision'];
|
|
203
|
+
const type = validTypes.includes(result.type) ? result.type : 'discovery';
|
|
204
|
+
|
|
205
|
+
// Validate concepts against fixed taxonomy
|
|
206
|
+
const concepts = Array.isArray(result.concepts)
|
|
207
|
+
? result.concepts.filter(c => VALID_CONCEPTS.includes(c)).slice(0, 3)
|
|
208
|
+
: [];
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
type,
|
|
212
|
+
title: (result.title || '').slice(0, 120),
|
|
213
|
+
narrative: (result.narrative || '').slice(0, 1000),
|
|
214
|
+
facts: Array.isArray(result.facts)
|
|
215
|
+
? result.facts.filter(f => typeof f === 'string').slice(0, 5)
|
|
216
|
+
: [],
|
|
217
|
+
concepts: concepts.length > 0 ? concepts : ['how-it-works']
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('[openclaw-mem] Tool extraction error:', err.message);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
type: 'discovery',
|
|
226
|
+
title: '',
|
|
227
|
+
narrative: '',
|
|
228
|
+
facts: [],
|
|
229
|
+
concepts: ['how-it-works']
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Batch extract concepts from multiple texts
|
|
235
|
+
*/
|
|
236
|
+
export async function batchExtractConcepts(texts) {
|
|
237
|
+
const results = new Map();
|
|
238
|
+
|
|
239
|
+
const uncached = [];
|
|
240
|
+
for (const text of texts) {
|
|
241
|
+
const cacheKey = getCacheKey(text);
|
|
242
|
+
const cached = conceptCache.get(cacheKey);
|
|
243
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
244
|
+
results.set(text, cached.concepts);
|
|
245
|
+
} else {
|
|
246
|
+
uncached.push(text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const BATCH_SIZE = 5;
|
|
251
|
+
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
|
|
252
|
+
const batch = uncached.slice(i, i + BATCH_SIZE);
|
|
253
|
+
const promises = batch.map(text => extractConcepts(text));
|
|
254
|
+
const batchResults = await Promise.all(promises);
|
|
255
|
+
|
|
256
|
+
for (let j = 0; j < batch.length; j++) {
|
|
257
|
+
results.set(batch[j], batchResults[j]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default {
|
|
265
|
+
extractConcepts,
|
|
266
|
+
extractFromToolCall,
|
|
267
|
+
batchExtractConcepts
|
|
268
|
+
};
|