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/handler.js
ADDED
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw-Mem Hook Handler
|
|
3
|
+
*
|
|
4
|
+
* Captures session content and provides memory context injection.
|
|
5
|
+
*
|
|
6
|
+
* Events handled:
|
|
7
|
+
* - command:new - Save session content before reset
|
|
8
|
+
* - gateway:startup - Initialize memory system
|
|
9
|
+
* - agent:bootstrap - Inject historical context
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { summarizeSession, INTERNAL_SUMMARY_PREFIX, callGatewayEmbeddings } from './gateway-llm.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
console.log('[openclaw-mem] >>> HANDLER LOADED AT', new Date().toISOString(), '<<<');
|
|
21
|
+
const USE_LLM_EXTRACTION = true;
|
|
22
|
+
const SUMMARY_MAX_MESSAGES = 200;
|
|
23
|
+
const MCP_API_PORT = 18790;
|
|
24
|
+
|
|
25
|
+
// Track API server process
|
|
26
|
+
let apiServerProcess = null;
|
|
27
|
+
let apiServerStarted = false;
|
|
28
|
+
|
|
29
|
+
// Avoid recursive memory capture for internal LLM runs
|
|
30
|
+
function isInternalSessionKey(sessionKey) {
|
|
31
|
+
if (!sessionKey || typeof sessionKey !== 'string') return false;
|
|
32
|
+
return sessionKey.startsWith(INTERNAL_SUMMARY_PREFIX);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Lazy load modules
|
|
36
|
+
let database = null;
|
|
37
|
+
let contextBuilder = null;
|
|
38
|
+
let extractor = null;
|
|
39
|
+
|
|
40
|
+
async function loadModules() {
|
|
41
|
+
if (database && contextBuilder) return true;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const dbModule = await import('./database.js');
|
|
45
|
+
const ctxModule = await import('./context-builder.js');
|
|
46
|
+
database = dbModule.default || dbModule.database;
|
|
47
|
+
contextBuilder = ctxModule.default || ctxModule;
|
|
48
|
+
|
|
49
|
+
if (USE_LLM_EXTRACTION) {
|
|
50
|
+
// Try to load extractor (optional, for LLM extraction)
|
|
51
|
+
try {
|
|
52
|
+
const extractorModule = await import('./extractor.js');
|
|
53
|
+
extractor = extractorModule.default || extractorModule;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.log('[openclaw-mem] LLM extractor not available, using basic extraction');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('[openclaw-mem] Failed to load modules:', err.message);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Generate UUID
|
|
67
|
+
function generateId() {
|
|
68
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
69
|
+
const r = Math.random() * 16 | 0;
|
|
70
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
71
|
+
return v.toString(16);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Estimate tokens
|
|
76
|
+
function estimateTokens(text) {
|
|
77
|
+
if (!text) return 0;
|
|
78
|
+
return Math.ceil(String(text).length / 4);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Simple hash function for content deduplication
|
|
82
|
+
function hashContent(text) {
|
|
83
|
+
if (!text) return '';
|
|
84
|
+
let hash = 0;
|
|
85
|
+
for (let i = 0; i < text.length; i++) {
|
|
86
|
+
const char = text.charCodeAt(i);
|
|
87
|
+
hash = ((hash << 5) - hash) + char;
|
|
88
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
89
|
+
}
|
|
90
|
+
return hash.toString(16);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read session transcript and extract conversation
|
|
95
|
+
*/
|
|
96
|
+
async function extractSessionContent(sessionFile, maxMessages = 20) {
|
|
97
|
+
try {
|
|
98
|
+
if (!sessionFile) return null;
|
|
99
|
+
|
|
100
|
+
const content = await fs.readFile(sessionFile, 'utf-8');
|
|
101
|
+
const lines = content.trim().split('\n');
|
|
102
|
+
|
|
103
|
+
const messages = [];
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
try {
|
|
106
|
+
const entry = JSON.parse(line);
|
|
107
|
+
if (entry.type === 'message' && entry.message) {
|
|
108
|
+
const msg = entry.message;
|
|
109
|
+
if ((msg.role === 'user' || msg.role === 'assistant') && msg.content) {
|
|
110
|
+
const text = Array.isArray(msg.content)
|
|
111
|
+
? msg.content.find(c => c.type === 'text')?.text
|
|
112
|
+
: msg.content;
|
|
113
|
+
|
|
114
|
+
if (text && !text.startsWith('/')) {
|
|
115
|
+
messages.push({
|
|
116
|
+
role: msg.role,
|
|
117
|
+
content: text.slice(0, 500) // Truncate long messages
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Skip invalid lines
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return messages.slice(-maxMessages);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[openclaw-mem] Failed to read session file:', err.message);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Start the MCP HTTP API server
|
|
136
|
+
*/
|
|
137
|
+
async function startApiServer() {
|
|
138
|
+
if (apiServerStarted) {
|
|
139
|
+
console.log('[openclaw-mem] API server already started');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if server is already running
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(`http://127.0.0.1:${MCP_API_PORT}/health`, {
|
|
146
|
+
signal: AbortSignal.timeout(1000)
|
|
147
|
+
});
|
|
148
|
+
if (response.ok) {
|
|
149
|
+
console.log('[openclaw-mem] API server already running on port', MCP_API_PORT);
|
|
150
|
+
apiServerStarted = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Server not running, start it
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const apiScript = path.join(__dirname, 'mcp-http-api.js');
|
|
158
|
+
const logDir = path.join(os.homedir(), '.openclaw-mem', 'logs');
|
|
159
|
+
|
|
160
|
+
// Ensure log directory exists
|
|
161
|
+
try {
|
|
162
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
163
|
+
} catch {}
|
|
164
|
+
|
|
165
|
+
const logFile = path.join(logDir, 'api.log');
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Start API server as detached process
|
|
169
|
+
apiServerProcess = spawn('node', [apiScript], {
|
|
170
|
+
detached: true,
|
|
171
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
172
|
+
env: { ...process.env, OPENCLAW_MEM_API_PORT: String(MCP_API_PORT) },
|
|
173
|
+
cwd: __dirname
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Log output to file
|
|
177
|
+
const logStream = await fs.open(logFile, 'a');
|
|
178
|
+
apiServerProcess.stdout?.on('data', (data) => {
|
|
179
|
+
logStream.write(data);
|
|
180
|
+
});
|
|
181
|
+
apiServerProcess.stderr?.on('data', (data) => {
|
|
182
|
+
logStream.write(data);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
apiServerProcess.unref();
|
|
186
|
+
apiServerStarted = true;
|
|
187
|
+
|
|
188
|
+
console.log(`[openclaw-mem] ✓ API server started on port ${MCP_API_PORT} (PID: ${apiServerProcess.pid})`);
|
|
189
|
+
|
|
190
|
+
// Wait for server to be ready
|
|
191
|
+
for (let i = 0; i < 10; i++) {
|
|
192
|
+
await new Promise(r => setTimeout(r, 200));
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(`http://127.0.0.1:${MCP_API_PORT}/health`, {
|
|
195
|
+
signal: AbortSignal.timeout(500)
|
|
196
|
+
});
|
|
197
|
+
if (response.ok) {
|
|
198
|
+
console.log('[openclaw-mem] ✓ API server is ready');
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error('[openclaw-mem] Failed to start API server:', err.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handle gateway:startup event
|
|
210
|
+
*/
|
|
211
|
+
async function handleGatewayStartup(event) {
|
|
212
|
+
console.log('[openclaw-mem] Gateway startup - initializing memory system');
|
|
213
|
+
|
|
214
|
+
if (!await loadModules()) return;
|
|
215
|
+
|
|
216
|
+
const stats = database.getStats();
|
|
217
|
+
console.log(`[openclaw-mem] Memory stats: ${stats.total_sessions} sessions, ${stats.total_observations} observations`);
|
|
218
|
+
|
|
219
|
+
// Start MCP HTTP API server
|
|
220
|
+
await startApiServer();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle agent:bootstrap event
|
|
225
|
+
* Inject historical context into new agent sessions
|
|
226
|
+
* AND capture incoming message to database
|
|
227
|
+
*/
|
|
228
|
+
async function handleAgentBootstrap(event) {
|
|
229
|
+
console.log('[openclaw-mem] Agent bootstrap:', event.sessionKey);
|
|
230
|
+
console.log('[openclaw-mem] Event keys:', Object.keys(event));
|
|
231
|
+
console.log('[openclaw-mem] Context exists:', !!event.context);
|
|
232
|
+
|
|
233
|
+
if (!await loadModules()) return;
|
|
234
|
+
|
|
235
|
+
// IMPORTANT: Ensure event.context exists (modify event directly, not a local copy)
|
|
236
|
+
if (!event.context) {
|
|
237
|
+
console.log('[openclaw-mem] WARNING: event.context is missing, creating it');
|
|
238
|
+
event.context = {};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('[openclaw-mem] Context keys:', Object.keys(event.context));
|
|
242
|
+
|
|
243
|
+
const workspaceDir = event.context.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
|
|
244
|
+
const sessionKey = event.sessionKey || 'unknown';
|
|
245
|
+
|
|
246
|
+
console.log('[openclaw-mem] workspaceDir:', workspaceDir);
|
|
247
|
+
console.log('[openclaw-mem] bootstrapFiles exists:', !!event.context.bootstrapFiles);
|
|
248
|
+
console.log('[openclaw-mem] bootstrapFiles is array:', Array.isArray(event.context.bootstrapFiles));
|
|
249
|
+
console.log('[openclaw-mem] bootstrapFiles length before:', event.context.bootstrapFiles?.length);
|
|
250
|
+
// Debug: show structure of first file
|
|
251
|
+
if (event.context.bootstrapFiles?.[0]) {
|
|
252
|
+
const sample = event.context.bootstrapFiles[0];
|
|
253
|
+
console.log('[openclaw-mem] Sample file keys:', Object.keys(sample));
|
|
254
|
+
console.log('[openclaw-mem] Sample file name:', sample.name);
|
|
255
|
+
console.log('[openclaw-mem] Sample has content:', !!sample.content);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Raw messages are no longer stored individually — session summaries capture the important bits.
|
|
259
|
+
// This eliminates noise from greetings and low-value messages.
|
|
260
|
+
console.log('[openclaw-mem] Skipping per-message capture (handled via session summary)');
|
|
261
|
+
|
|
262
|
+
// Ensure API server is running
|
|
263
|
+
await startApiServer();
|
|
264
|
+
|
|
265
|
+
// Build context to inject (async for LLM extraction)
|
|
266
|
+
const memContext = await contextBuilder.buildContext(workspaceDir, {
|
|
267
|
+
observationLimit: 30,
|
|
268
|
+
fullDetailCount: 3,
|
|
269
|
+
useLLMExtraction: USE_LLM_EXTRACTION
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Build tool instructions for memory retrieval
|
|
273
|
+
const toolInstructions = `
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 🧠 记忆检索工具(重要:必须使用)
|
|
277
|
+
|
|
278
|
+
**⚠️ 当用户问到以下情况时,你必须先调用脚本获取详情,再回答:**
|
|
279
|
+
- "具体内容是什么"、"详细步骤"、"完整版"
|
|
280
|
+
- "之前说过的 XXX 具体是什么"
|
|
281
|
+
- 上面的摘要只有标题,用户想要完整内容
|
|
282
|
+
- 任何需要超出摘要范围的细节
|
|
283
|
+
|
|
284
|
+
### 使用方法(使用脚本,自动处理中文编码)
|
|
285
|
+
|
|
286
|
+
**1. 搜索记忆(找到相关 ID)**
|
|
287
|
+
\`\`\`bash
|
|
288
|
+
~/.openclaw/hooks/openclaw-mem/mem-search.sh "关键词" 10
|
|
289
|
+
\`\`\`
|
|
290
|
+
|
|
291
|
+
**2. 获取完整详情(用找到的 ID)**
|
|
292
|
+
\`\`\`bash
|
|
293
|
+
~/.openclaw/hooks/openclaw-mem/mem-get.sh ID1 ID2
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
### 示例
|
|
297
|
+
|
|
298
|
+
用户问:"之前讨论的效率提升方法具体有哪些步骤?"
|
|
299
|
+
|
|
300
|
+
你应该:
|
|
301
|
+
1. 搜索: \`~/.openclaw/hooks/openclaw-mem/mem-search.sh "效率提升" 10\`
|
|
302
|
+
2. 获取详情: \`~/.openclaw/hooks/openclaw-mem/mem-get.sh 535 526\`(用搜索到的 ID)
|
|
303
|
+
3. 根据返回的完整内容回答用户
|
|
304
|
+
|
|
305
|
+
**不要仅凭摘要回答需要详情的问题!**
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
if (memContext) {
|
|
309
|
+
console.log(`[openclaw-mem] Built context: ${memContext.length} chars`);
|
|
310
|
+
|
|
311
|
+
// Strategy: Write memory context to a dedicated file on disk
|
|
312
|
+
// This ensures AI can read it with the Read tool
|
|
313
|
+
const memContextFile = path.join(workspaceDir, 'SESSION-MEMORY.md');
|
|
314
|
+
try {
|
|
315
|
+
await fs.writeFile(memContextFile, memContext + toolInstructions, 'utf-8');
|
|
316
|
+
console.log(`[openclaw-mem] ✓ Written SESSION-MEMORY.md to disk (${memContext.length} chars + tool instructions)`);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error('[openclaw-mem] Failed to write SESSION-MEMORY.md:', err.message);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Also modify bootstrapFiles array for system prompt injection
|
|
322
|
+
if (event.context.bootstrapFiles && Array.isArray(event.context.bootstrapFiles)) {
|
|
323
|
+
const memoryFile = event.context.bootstrapFiles.find(f => f.name === 'MEMORY.md');
|
|
324
|
+
if (memoryFile && memoryFile.content && !memoryFile.missing) {
|
|
325
|
+
memoryFile.content = memoryFile.content + '\n\n---\n\n# Session Memory\n\nSee SESSION-MEMORY.md for recent activity and conversation history.\n\n' + memContext + toolInstructions;
|
|
326
|
+
console.log('[openclaw-mem] ✓ Appended to MEMORY.md in bootstrapFiles');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
console.log('[openclaw-mem] No context to inject (empty memory)');
|
|
331
|
+
// Still write tool instructions even if no context
|
|
332
|
+
const memContextFile = path.join(workspaceDir, 'SESSION-MEMORY.md');
|
|
333
|
+
try {
|
|
334
|
+
await fs.writeFile(memContextFile, '# Session Memory\n\nNo recent observations found.\n' + toolInstructions, 'utf-8');
|
|
335
|
+
console.log('[openclaw-mem] ✓ Written SESSION-MEMORY.md with tool instructions only');
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error('[openclaw-mem] Failed to write SESSION-MEMORY.md:', err.message);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Track active sessions by sessionKey
|
|
343
|
+
const activeSessions = new Map();
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get or create a session ID for a given sessionKey
|
|
347
|
+
*/
|
|
348
|
+
function getOrCreateSessionForKey(sessionKey, workspaceDir) {
|
|
349
|
+
if (activeSessions.has(sessionKey)) {
|
|
350
|
+
return activeSessions.get(sessionKey);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const sessionId = generateId();
|
|
354
|
+
database.createSession(sessionId, workspaceDir, sessionKey, 'bootstrap');
|
|
355
|
+
activeSessions.set(sessionKey, sessionId);
|
|
356
|
+
|
|
357
|
+
// Clean up old sessions after 1 hour
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
if (activeSessions.get(sessionKey) === sessionId) {
|
|
360
|
+
activeSessions.delete(sessionKey);
|
|
361
|
+
database.endSession(sessionId);
|
|
362
|
+
}
|
|
363
|
+
}, 60 * 60 * 1000);
|
|
364
|
+
|
|
365
|
+
console.log(`[openclaw-mem] Created new session ${sessionId} for key ${sessionKey}`);
|
|
366
|
+
return sessionId;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Handle command:new event
|
|
371
|
+
* Save session content before reset
|
|
372
|
+
*/
|
|
373
|
+
async function handleCommandNew(event) {
|
|
374
|
+
console.log('[openclaw-mem] Command new - saving session');
|
|
375
|
+
|
|
376
|
+
if (!await loadModules()) return;
|
|
377
|
+
|
|
378
|
+
const context = event.context || {};
|
|
379
|
+
const sessionKey = event.sessionKey || 'unknown';
|
|
380
|
+
const sessionId = generateId();
|
|
381
|
+
|
|
382
|
+
// Get workspace and session info
|
|
383
|
+
const workspaceDir = context.workspaceDir ||
|
|
384
|
+
context.cfg?.agents?.defaults?.workspace ||
|
|
385
|
+
path.join(os.homedir(), '.openclaw', 'workspace');
|
|
386
|
+
|
|
387
|
+
const sessionEntry = context.previousSessionEntry || context.sessionEntry || {};
|
|
388
|
+
const sessionFile = sessionEntry.sessionFile;
|
|
389
|
+
|
|
390
|
+
// Create session record
|
|
391
|
+
database.createSession(sessionId, workspaceDir, sessionKey, context.commandSource || 'command');
|
|
392
|
+
|
|
393
|
+
// Extract session content
|
|
394
|
+
const messages = await extractSessionContent(sessionFile, 20);
|
|
395
|
+
|
|
396
|
+
if (messages && messages.length > 0) {
|
|
397
|
+
console.log(`[openclaw-mem] Extracted ${messages.length} messages from session`);
|
|
398
|
+
// Raw messages are no longer stored individually — only the AI summary matters.
|
|
399
|
+
console.log('[openclaw-mem] Generating AI summary...');
|
|
400
|
+
|
|
401
|
+
// Generate AI summary using DeepSeek
|
|
402
|
+
let aiSummary = null;
|
|
403
|
+
try {
|
|
404
|
+
aiSummary = await summarizeSession(messages, { sessionKey });
|
|
405
|
+
console.log('[openclaw-mem] Kimi summary result:', aiSummary ? 'success' : 'null');
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.error('[openclaw-mem] Kimi summary error:', err.message);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (aiSummary && (aiSummary.request || aiSummary.learned || aiSummary.completed || aiSummary.next_steps)) {
|
|
411
|
+
const summaryContent = JSON.stringify(aiSummary);
|
|
412
|
+
database.saveSummary(
|
|
413
|
+
sessionId,
|
|
414
|
+
summaryContent,
|
|
415
|
+
aiSummary.request,
|
|
416
|
+
aiSummary.investigated || null,
|
|
417
|
+
aiSummary.learned,
|
|
418
|
+
aiSummary.completed,
|
|
419
|
+
aiSummary.next_steps
|
|
420
|
+
);
|
|
421
|
+
console.log('[openclaw-mem] ✓ AI summary saved');
|
|
422
|
+
} else {
|
|
423
|
+
// Fallback summary
|
|
424
|
+
const userMessages = messages.filter(m => m.role === 'user');
|
|
425
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
426
|
+
const fallbackRequest = userMessages[0]?.content?.slice(0, 200) || 'Session started';
|
|
427
|
+
const fallbackCompleted = assistantMessages.slice(-1)[0]?.content?.slice(0, 200) || '';
|
|
428
|
+
|
|
429
|
+
database.saveSummary(
|
|
430
|
+
sessionId,
|
|
431
|
+
`Session with ${messages.length} messages`,
|
|
432
|
+
fallbackRequest,
|
|
433
|
+
null,
|
|
434
|
+
null,
|
|
435
|
+
fallbackCompleted ? `Discussed: ${fallbackCompleted}` : null,
|
|
436
|
+
null
|
|
437
|
+
);
|
|
438
|
+
console.log('[openclaw-mem] ✓ Fallback summary saved');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// End session
|
|
443
|
+
database.endSession(sessionId);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Handle agent:response event
|
|
448
|
+
* Skip storing raw assistant messages — session summary at stop/new captures the important bits.
|
|
449
|
+
* This avoids noise from greetings, acknowledgments, and other low-value messages.
|
|
450
|
+
*/
|
|
451
|
+
async function handleAgentResponse(event) {
|
|
452
|
+
console.log('[openclaw-mem] Agent response event (skipped — captured via session summary)');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Handle message events
|
|
457
|
+
* Skip storing raw messages — session summary at stop/new captures the important bits.
|
|
458
|
+
* This avoids noise from greetings, acknowledgments, and other low-value messages.
|
|
459
|
+
*/
|
|
460
|
+
async function handleMessage(event) {
|
|
461
|
+
console.log('[openclaw-mem] Message event (skipped — captured via session summary)');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check if content should be excluded from memory (privacy protection)
|
|
466
|
+
*/
|
|
467
|
+
function shouldExclude(content) {
|
|
468
|
+
if (!content || typeof content !== 'string') return false;
|
|
469
|
+
|
|
470
|
+
// <private> tag exclusion
|
|
471
|
+
if (content.includes('<private>') || content.includes('</private>')) return true;
|
|
472
|
+
|
|
473
|
+
// Sensitive file patterns
|
|
474
|
+
const sensitivePatterns = [
|
|
475
|
+
'.env',
|
|
476
|
+
'credentials',
|
|
477
|
+
'secret',
|
|
478
|
+
'password',
|
|
479
|
+
'api_key',
|
|
480
|
+
'apikey',
|
|
481
|
+
'api-key',
|
|
482
|
+
'private_key',
|
|
483
|
+
'privatekey',
|
|
484
|
+
'access_token',
|
|
485
|
+
'accesstoken',
|
|
486
|
+
'auth_token',
|
|
487
|
+
'authtoken'
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
const lowerContent = content.toLowerCase();
|
|
491
|
+
for (const pattern of sensitivePatterns) {
|
|
492
|
+
if (lowerContent.includes(pattern)) return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Extract files read from tool call
|
|
500
|
+
*/
|
|
501
|
+
function extractFilesRead(toolName, toolInput) {
|
|
502
|
+
if (!toolInput) return [];
|
|
503
|
+
|
|
504
|
+
switch (toolName) {
|
|
505
|
+
case 'Read':
|
|
506
|
+
return toolInput.file_path ? [toolInput.file_path] : [];
|
|
507
|
+
case 'Grep':
|
|
508
|
+
return toolInput.path ? [toolInput.path] : [];
|
|
509
|
+
case 'Glob':
|
|
510
|
+
// Glob returns matched files, but input doesn't contain them
|
|
511
|
+
return [];
|
|
512
|
+
default:
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Extract files modified from tool call
|
|
519
|
+
*/
|
|
520
|
+
function extractFilesModified(toolName, toolInput) {
|
|
521
|
+
if (!toolInput) return [];
|
|
522
|
+
|
|
523
|
+
switch (toolName) {
|
|
524
|
+
case 'Edit':
|
|
525
|
+
return toolInput.file_path ? [toolInput.file_path] : [];
|
|
526
|
+
case 'Write':
|
|
527
|
+
return toolInput.file_path ? [toolInput.file_path] : [];
|
|
528
|
+
case 'NotebookEdit':
|
|
529
|
+
return toolInput.notebook_path ? [toolInput.notebook_path] : [];
|
|
530
|
+
default:
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Classify tool call type
|
|
537
|
+
*/
|
|
538
|
+
function classifyToolType(toolName, toolInput, toolResponse) {
|
|
539
|
+
// File modification tools
|
|
540
|
+
if (['Edit', 'Write', 'NotebookEdit'].includes(toolName)) {
|
|
541
|
+
return 'modification';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// File reading tools
|
|
545
|
+
if (['Read', 'Grep', 'Glob'].includes(toolName)) {
|
|
546
|
+
return 'discovery';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Command execution
|
|
550
|
+
if (toolName === 'Bash') {
|
|
551
|
+
const command = toolInput?.command || '';
|
|
552
|
+
if (command.includes('git commit') || command.includes('git push')) {
|
|
553
|
+
return 'commit';
|
|
554
|
+
}
|
|
555
|
+
if (command.includes('npm test') || command.includes('pytest') || command.includes('jest')) {
|
|
556
|
+
return 'testing';
|
|
557
|
+
}
|
|
558
|
+
if (command.includes('npm install') || command.includes('pip install')) {
|
|
559
|
+
return 'setup';
|
|
560
|
+
}
|
|
561
|
+
return 'command';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Web tools
|
|
565
|
+
if (['WebFetch', 'WebSearch'].includes(toolName)) {
|
|
566
|
+
return 'research';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Task/Agent tools
|
|
570
|
+
if (toolName === 'Task') {
|
|
571
|
+
return 'delegation';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return 'other';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Handle tool:post event
|
|
579
|
+
* Records every tool call for memory tracking
|
|
580
|
+
*/
|
|
581
|
+
async function handleToolPost(event) {
|
|
582
|
+
console.log('[openclaw-mem] Tool post event');
|
|
583
|
+
|
|
584
|
+
if (!await loadModules()) return;
|
|
585
|
+
|
|
586
|
+
const toolName = event.tool_name || event.toolName || 'Unknown';
|
|
587
|
+
const toolInput = event.tool_input || event.toolInput || event.input || {};
|
|
588
|
+
const toolResponse = event.tool_response || event.toolResponse || event.response || event.output || {};
|
|
589
|
+
const sessionKey = event.sessionKey || 'unknown';
|
|
590
|
+
const workspaceDir = event.context?.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
|
|
591
|
+
|
|
592
|
+
// Skip certain tools that generate noise
|
|
593
|
+
const skipTools = ['AskUserQuestion', 'TaskList', 'TaskGet'];
|
|
594
|
+
if (skipTools.includes(toolName)) {
|
|
595
|
+
console.log(`[openclaw-mem] Skipping ${toolName} (noise filter)`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Privacy check - skip sensitive content
|
|
600
|
+
const inputStr = JSON.stringify(toolInput);
|
|
601
|
+
const responseStr = JSON.stringify(toolResponse);
|
|
602
|
+
if (shouldExclude(inputStr) || shouldExclude(responseStr)) {
|
|
603
|
+
console.log(`[openclaw-mem] Skipping ${toolName} (privacy filter)`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Extract metadata
|
|
608
|
+
const filesRead = extractFilesRead(toolName, toolInput);
|
|
609
|
+
const filesModified = extractFilesModified(toolName, toolInput);
|
|
610
|
+
const toolType = classifyToolType(toolName, toolInput, toolResponse);
|
|
611
|
+
|
|
612
|
+
// Build summary
|
|
613
|
+
let summary = '';
|
|
614
|
+
if (toolInput.file_path) {
|
|
615
|
+
summary = `${toolName}: ${toolInput.file_path}`;
|
|
616
|
+
} else if (toolInput.command) {
|
|
617
|
+
summary = `${toolName}: ${toolInput.command.slice(0, 80)}`;
|
|
618
|
+
} else if (toolInput.pattern) {
|
|
619
|
+
summary = `${toolName}: ${toolInput.pattern}`;
|
|
620
|
+
} else if (toolInput.query) {
|
|
621
|
+
summary = `${toolName}: ${toolInput.query.slice(0, 80)}`;
|
|
622
|
+
} else if (toolInput.url) {
|
|
623
|
+
summary = `${toolName}: ${toolInput.url}`;
|
|
624
|
+
} else {
|
|
625
|
+
summary = `${toolName} operation`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Build basic narrative (fallback)
|
|
629
|
+
let narrative = '';
|
|
630
|
+
if (filesModified.length > 0) {
|
|
631
|
+
narrative = `Modified ${filesModified.join(', ')}`;
|
|
632
|
+
} else if (filesRead.length > 0) {
|
|
633
|
+
narrative = `Read ${filesRead.join(', ')}`;
|
|
634
|
+
} else if (toolInput.command) {
|
|
635
|
+
narrative = `Executed command: ${toolInput.command.slice(0, 100)}`;
|
|
636
|
+
} else if (toolInput.query) {
|
|
637
|
+
narrative = `Searched for: ${toolInput.query}`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Get or create session
|
|
641
|
+
let sessionId = getOrCreateSessionForKey(sessionKey, workspaceDir);
|
|
642
|
+
|
|
643
|
+
// Try LLM extraction for richer metadata
|
|
644
|
+
let extractedType = toolType;
|
|
645
|
+
let extractedNarrative = narrative;
|
|
646
|
+
let extractedFacts = null;
|
|
647
|
+
let extractedConcepts = `${toolName} ${summary}`.slice(0, 500);
|
|
648
|
+
|
|
649
|
+
if (USE_LLM_EXTRACTION && extractor && extractor.extractFromToolCall) {
|
|
650
|
+
try {
|
|
651
|
+
const extracted = await extractor.extractFromToolCall({
|
|
652
|
+
tool_name: toolName,
|
|
653
|
+
tool_input: toolInput,
|
|
654
|
+
tool_response: toolResponse,
|
|
655
|
+
filesRead,
|
|
656
|
+
filesModified
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (extracted) {
|
|
660
|
+
extractedType = extracted.type || toolType;
|
|
661
|
+
extractedNarrative = extracted.narrative || narrative;
|
|
662
|
+
extractedFacts = extracted.facts;
|
|
663
|
+
extractedConcepts = extracted.concepts?.join(', ') || extractedConcepts;
|
|
664
|
+
// Use LLM-generated title as summary if available
|
|
665
|
+
if (extracted.title) {
|
|
666
|
+
summary = extracted.title;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
console.log(`[openclaw-mem] LLM extracted: type=${extractedType}, title=${summary.slice(0, 60)}, concepts=${extractedConcepts}`);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.log(`[openclaw-mem] LLM extraction failed, using fallback: ${err.message}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Save observation with extended metadata
|
|
676
|
+
const saveResult = database.saveObservation(
|
|
677
|
+
sessionId,
|
|
678
|
+
toolName,
|
|
679
|
+
toolInput,
|
|
680
|
+
toolResponse,
|
|
681
|
+
{
|
|
682
|
+
summary: summary.slice(0, 200),
|
|
683
|
+
concepts: extractedConcepts,
|
|
684
|
+
tokensDiscovery: estimateTokens(responseStr),
|
|
685
|
+
tokensRead: estimateTokens(summary),
|
|
686
|
+
type: extractedType,
|
|
687
|
+
narrative: extractedNarrative.slice(0, 1000),
|
|
688
|
+
facts: extractedFacts,
|
|
689
|
+
filesRead: filesRead,
|
|
690
|
+
filesModified: filesModified
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
console.log(`[openclaw-mem] ✓ Tool ${toolName} recorded (type: ${extractedType})`);
|
|
695
|
+
|
|
696
|
+
// Fire-and-forget: generate embedding for the new observation
|
|
697
|
+
if (saveResult.success && saveResult.id) {
|
|
698
|
+
const embeddingText = [summary, extractedNarrative].filter(Boolean).join(' ').trim();
|
|
699
|
+
if (embeddingText.length > 10) {
|
|
700
|
+
callGatewayEmbeddings(embeddingText).then(embedding => {
|
|
701
|
+
if (embedding) {
|
|
702
|
+
database.saveEmbedding(Number(saveResult.id), embedding);
|
|
703
|
+
console.log(`[openclaw-mem] ✓ Embedding saved for observation #${saveResult.id}`);
|
|
704
|
+
}
|
|
705
|
+
}).catch(err => {
|
|
706
|
+
console.log(`[openclaw-mem] Embedding generation failed: ${err.message}`);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Handle user:prompt:submit event (UserPromptSubmit)
|
|
714
|
+
* Records user prompts to user_prompts table
|
|
715
|
+
*/
|
|
716
|
+
async function handleUserPromptSubmit(event) {
|
|
717
|
+
console.log('[openclaw-mem] User prompt submit event');
|
|
718
|
+
|
|
719
|
+
if (!await loadModules()) return;
|
|
720
|
+
|
|
721
|
+
const sessionKey = event.sessionKey || 'unknown';
|
|
722
|
+
const workspaceDir = event.context?.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
|
|
723
|
+
|
|
724
|
+
// Extract user prompt from various possible locations in event
|
|
725
|
+
const prompt = event.prompt || event.content || event.message || event.text || event.input;
|
|
726
|
+
|
|
727
|
+
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
728
|
+
console.log('[openclaw-mem] No prompt content found in event');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Skip slash commands
|
|
733
|
+
if (prompt.trim().startsWith('/')) {
|
|
734
|
+
console.log('[openclaw-mem] Skipping slash command');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Privacy check
|
|
739
|
+
if (shouldExclude(prompt)) {
|
|
740
|
+
console.log('[openclaw-mem] Skipping prompt (privacy filter)');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Get or create session
|
|
745
|
+
const sessionId = getOrCreateSessionForKey(sessionKey, workspaceDir);
|
|
746
|
+
|
|
747
|
+
// Save to user_prompts table
|
|
748
|
+
database.saveUserPrompt(sessionId, prompt);
|
|
749
|
+
console.log(`[openclaw-mem] ✓ User prompt saved (${prompt.slice(0, 50)}...)`);
|
|
750
|
+
|
|
751
|
+
// User prompts are saved to user_prompts table only (no observation duplication).
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Handle agent:stop event (Stop)
|
|
756
|
+
* Called when the model stops/completes a turn
|
|
757
|
+
*/
|
|
758
|
+
async function handleAgentStop(event) {
|
|
759
|
+
console.log('[openclaw-mem] Agent stop event');
|
|
760
|
+
|
|
761
|
+
if (!await loadModules()) return;
|
|
762
|
+
|
|
763
|
+
const sessionKey = event.sessionKey || 'unknown';
|
|
764
|
+
const workspaceDir = event.context?.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
|
|
765
|
+
const stopReason = event.reason || event.stop_reason || 'unknown';
|
|
766
|
+
|
|
767
|
+
// Get session
|
|
768
|
+
const sessionId = getOrCreateSessionForKey(sessionKey, workspaceDir);
|
|
769
|
+
|
|
770
|
+
// Record the stop event as an observation
|
|
771
|
+
const summary = `Agent stopped: ${stopReason}`;
|
|
772
|
+
database.saveObservation(
|
|
773
|
+
sessionId,
|
|
774
|
+
'AgentStop',
|
|
775
|
+
{ reason: stopReason, sessionKey },
|
|
776
|
+
{ stopped: true, timestamp: new Date().toISOString() },
|
|
777
|
+
{
|
|
778
|
+
summary,
|
|
779
|
+
concepts: `stop, ${stopReason}`,
|
|
780
|
+
tokensDiscovery: 10,
|
|
781
|
+
tokensRead: 5,
|
|
782
|
+
type: 'lifecycle',
|
|
783
|
+
narrative: `Agent turn completed with reason: ${stopReason}`,
|
|
784
|
+
facts: null,
|
|
785
|
+
filesRead: null,
|
|
786
|
+
filesModified: null
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
console.log(`[openclaw-mem] ✓ Agent stop recorded (reason: ${stopReason})`);
|
|
791
|
+
|
|
792
|
+
// Generate summary on stop (Claude-Mem parity)
|
|
793
|
+
try {
|
|
794
|
+
const existing = database.getSummaryBySession(sessionId);
|
|
795
|
+
if (existing) {
|
|
796
|
+
console.log('[openclaw-mem] Summary already exists for session, skipping stop summary');
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const agentId = event.context?.agentId || 'main';
|
|
801
|
+
const sessionFile = path.join(os.homedir(), '.openclaw', 'agents', agentId, 'sessions', `${sessionKey}.jsonl`);
|
|
802
|
+
let sessionFileExists = false;
|
|
803
|
+
try {
|
|
804
|
+
await fs.access(sessionFile);
|
|
805
|
+
sessionFileExists = true;
|
|
806
|
+
} catch {
|
|
807
|
+
sessionFileExists = false;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!sessionFileExists) {
|
|
811
|
+
console.log('[openclaw-mem] No session file found for stop summary');
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const messages = await extractSessionContent(sessionFile, SUMMARY_MAX_MESSAGES);
|
|
816
|
+
if (!messages || messages.length === 0) {
|
|
817
|
+
console.log('[openclaw-mem] No messages found for stop summary');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let summary = null;
|
|
822
|
+
try {
|
|
823
|
+
summary = await summarizeSession(messages, { sessionKey });
|
|
824
|
+
} catch {
|
|
825
|
+
summary = null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (summary && (summary.request || summary.learned || summary.completed || summary.next_steps)) {
|
|
829
|
+
const summaryContent = JSON.stringify(summary);
|
|
830
|
+
database.saveSummary(
|
|
831
|
+
sessionId,
|
|
832
|
+
summaryContent,
|
|
833
|
+
summary.request,
|
|
834
|
+
summary.investigated || null,
|
|
835
|
+
summary.learned,
|
|
836
|
+
summary.completed,
|
|
837
|
+
summary.next_steps
|
|
838
|
+
);
|
|
839
|
+
console.log('[openclaw-mem] ✓ Stop summary saved');
|
|
840
|
+
} else {
|
|
841
|
+
// Fallback summary if LLM failed
|
|
842
|
+
const userMessages = messages.filter(m => m.role === 'user');
|
|
843
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
844
|
+
const summaryContent = `Session with ${messages.length} messages (${userMessages.length} user, ${assistantMessages.length} assistant)`;
|
|
845
|
+
const firstUserMsg = userMessages[0]?.content?.slice(0, 200) || '';
|
|
846
|
+
const lastAssistant = assistantMessages.slice(-1)[0]?.content?.slice(0, 100) || 'various topics';
|
|
847
|
+
|
|
848
|
+
database.saveSummary(
|
|
849
|
+
sessionId,
|
|
850
|
+
summaryContent,
|
|
851
|
+
firstUserMsg,
|
|
852
|
+
null,
|
|
853
|
+
null,
|
|
854
|
+
`Discussed: ${lastAssistant}`,
|
|
855
|
+
null
|
|
856
|
+
);
|
|
857
|
+
console.log('[openclaw-mem] ✓ Stop summary saved (fallback)');
|
|
858
|
+
}
|
|
859
|
+
} catch (err) {
|
|
860
|
+
console.error('[openclaw-mem] Stop summary error:', err.message);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// If this is an end_turn or max_tokens stop, we might want to
|
|
864
|
+
// trigger a summary generation for the turn
|
|
865
|
+
if (stopReason === 'end_turn' || stopReason === 'stop_sequence') {
|
|
866
|
+
console.log('[openclaw-mem] Turn completed normally');
|
|
867
|
+
} else if (stopReason === 'max_tokens') {
|
|
868
|
+
console.log('[openclaw-mem] Turn stopped due to max tokens');
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Main hook handler
|
|
874
|
+
*/
|
|
875
|
+
const openclawMemHandler = async (event) => {
|
|
876
|
+
const eventType = event.type;
|
|
877
|
+
const eventAction = event.action;
|
|
878
|
+
const eventSessionKey = event.sessionKey;
|
|
879
|
+
|
|
880
|
+
if (isInternalSessionKey(eventSessionKey)) {
|
|
881
|
+
console.log('[openclaw-mem] Skipping internal session:', eventSessionKey);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
console.log('[openclaw-mem] Event:', eventType, eventAction || '', '(v2026-02-03-1629)');
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
if (eventType === 'gateway' && eventAction === 'startup') {
|
|
889
|
+
await handleGatewayStartup(event);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (eventType === 'agent' && eventAction === 'bootstrap') {
|
|
894
|
+
await handleAgentBootstrap(event);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (eventType === 'command' && eventAction === 'new') {
|
|
899
|
+
await handleCommandNew(event);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Handle agent response to capture assistant messages
|
|
904
|
+
if (eventType === 'agent' && eventAction === 'response') {
|
|
905
|
+
await handleAgentResponse(event);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Handle tool:post events to capture tool calls
|
|
910
|
+
if (eventType === 'tool' && eventAction === 'post') {
|
|
911
|
+
await handleToolPost(event);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Handle user:prompt:submit (UserPromptSubmit) - when user submits a prompt
|
|
916
|
+
if ((eventType === 'user' && eventAction === 'prompt') ||
|
|
917
|
+
(eventType === 'prompt' && eventAction === 'submit') ||
|
|
918
|
+
(eventType === 'user' && eventAction === 'submit')) {
|
|
919
|
+
await handleUserPromptSubmit(event);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Handle agent:stop (Stop) - when model stops/completes a turn
|
|
924
|
+
if (eventType === 'agent' && eventAction === 'stop') {
|
|
925
|
+
await handleAgentStop(event);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Handle message events (alternative event type)
|
|
930
|
+
if (eventType === 'message') {
|
|
931
|
+
await handleMessage(event);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
} catch (err) {
|
|
936
|
+
console.error('[openclaw-mem] Handler error:', err.message);
|
|
937
|
+
console.error(err.stack);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
export default openclawMemHandler;
|