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.
Files changed (47) hide show
  1. package/HOOK.md +125 -0
  2. package/LICENSE +1 -1
  3. package/MCP.json +11 -0
  4. package/README.md +158 -167
  5. package/backfill-embeddings.js +79 -0
  6. package/context-builder.js +703 -0
  7. package/database.js +625 -0
  8. package/debug-logger.js +280 -0
  9. package/extractor.js +268 -0
  10. package/gateway-llm.js +250 -0
  11. package/handler.js +941 -0
  12. package/mcp-http-api.js +424 -0
  13. package/mcp-server.js +605 -0
  14. package/mem-get.sh +24 -0
  15. package/mem-search.sh +17 -0
  16. package/monitor.js +112 -0
  17. package/package.json +58 -30
  18. package/realtime-monitor.js +371 -0
  19. package/session-watcher.js +192 -0
  20. package/setup.js +114 -0
  21. package/sync-recent.js +63 -0
  22. package/README_CN.md +0 -201
  23. package/bin/openclaw-mem.js +0 -117
  24. package/docs/locales/README_AR.md +0 -35
  25. package/docs/locales/README_DE.md +0 -35
  26. package/docs/locales/README_ES.md +0 -35
  27. package/docs/locales/README_FR.md +0 -35
  28. package/docs/locales/README_HE.md +0 -35
  29. package/docs/locales/README_HI.md +0 -35
  30. package/docs/locales/README_ID.md +0 -35
  31. package/docs/locales/README_IT.md +0 -35
  32. package/docs/locales/README_JA.md +0 -57
  33. package/docs/locales/README_KO.md +0 -35
  34. package/docs/locales/README_NL.md +0 -35
  35. package/docs/locales/README_PL.md +0 -35
  36. package/docs/locales/README_PT.md +0 -35
  37. package/docs/locales/README_RU.md +0 -35
  38. package/docs/locales/README_TH.md +0 -35
  39. package/docs/locales/README_TR.md +0 -35
  40. package/docs/locales/README_UK.md +0 -35
  41. package/docs/locales/README_VI.md +0 -35
  42. package/docs/logo.svg +0 -32
  43. package/lib/context-builder.js +0 -415
  44. package/lib/database.js +0 -309
  45. package/lib/handler.js +0 -494
  46. package/scripts/commands.js +0 -141
  47. package/scripts/init.js +0 -248
package/gateway-llm.js ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * DeepSeek LLM helper
3
+ * Calls the DeepSeek OpenAI-compatible endpoint to summarize sessions.
4
+ */
5
+
6
+ const SUMMARY_SESSION_PREFIX = 'mem-summary:';
7
+ const DEFAULT_DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
8
+ const DEFAULT_DEEPSEEK_MODEL = 'deepseek-chat';
9
+
10
+ function getDeepSeekBaseUrl() {
11
+ return process.env.DEEPSEEK_BASE_URL
12
+ || DEFAULT_DEEPSEEK_BASE_URL;
13
+ }
14
+
15
+ function getDeepSeekApiKey() {
16
+ return process.env.DEEPSEEK_API_KEY || '';
17
+ }
18
+
19
+ function getDeepSeekModel() {
20
+ return process.env.DEEPSEEK_MODEL
21
+ || DEFAULT_DEEPSEEK_MODEL;
22
+ }
23
+
24
+ function truncateText(text, maxChars) {
25
+ if (!text) return '';
26
+ if (text.length <= maxChars) return text;
27
+ return text.slice(0, maxChars) + '…';
28
+ }
29
+
30
+ function formatTranscript(messages, maxChars = 12000) {
31
+ const lines = [];
32
+ for (const m of messages) {
33
+ const role = (m.role || 'unknown').toUpperCase();
34
+ const content = String(m.content || '').replace(/\s+/g, ' ').trim();
35
+ if (!content) continue;
36
+ lines.push(`${role}: ${content}`);
37
+ }
38
+ return truncateText(lines.join('\n'), maxChars);
39
+ }
40
+
41
+ function parseSummaryJson(text) {
42
+ if (!text) return null;
43
+ const match = text.match(/\{[\s\S]*\}/);
44
+ if (!match) return null;
45
+ try {
46
+ const obj = JSON.parse(match[0]);
47
+ return obj && typeof obj === 'object' ? obj : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function normalizeSummaryFields(obj) {
54
+ if (!obj) return null;
55
+ const pick = (key) => {
56
+ const val = obj[key];
57
+ if (typeof val === 'string') return val.trim();
58
+ if (val == null) return '';
59
+ return String(val).trim();
60
+ };
61
+ return {
62
+ request: pick('request'),
63
+ investigated: pick('investigated'),
64
+ learned: pick('learned'),
65
+ completed: pick('completed'),
66
+ next_steps: pick('next_steps')
67
+ };
68
+ }
69
+
70
+ async function callGatewayChat(messages, options = {}) {
71
+ const {
72
+ sessionKey = 'unknown',
73
+ temperature = 0.2,
74
+ max_tokens = 300,
75
+ model
76
+ } = options;
77
+ const apiKey = getDeepSeekApiKey();
78
+ if (!apiKey) {
79
+ console.log('[openclaw-mem] No DEEPSEEK_API_KEY found');
80
+ return null;
81
+ }
82
+ const baseUrl = getDeepSeekBaseUrl();
83
+ const resolvedModel = model || getDeepSeekModel();
84
+ const url = `${baseUrl}/chat/completions`;
85
+ const payload = {
86
+ model: resolvedModel,
87
+ stream: false,
88
+ temperature,
89
+ max_tokens,
90
+ messages
91
+ };
92
+
93
+ const headers = {
94
+ 'Content-Type': 'application/json',
95
+ 'Authorization': `Bearer ${apiKey}`
96
+ };
97
+
98
+ try {
99
+ console.log('[openclaw-mem] Calling DeepSeek API...');
100
+ const res = await fetch(url, {
101
+ method: 'POST',
102
+ headers,
103
+ body: JSON.stringify(payload)
104
+ });
105
+ if (!res.ok) {
106
+ const errText = await res.text();
107
+ console.error('[openclaw-mem] DeepSeek API error:', res.status, errText);
108
+ return null;
109
+ }
110
+ const json = await res.json();
111
+ const content = json?.choices?.[0]?.message?.content || '';
112
+ console.log('[openclaw-mem] DeepSeek response received');
113
+ return content;
114
+ } catch (err) {
115
+ console.error('[openclaw-mem] DeepSeek fetch error:', err.message);
116
+ return null;
117
+ }
118
+ }
119
+
120
+ export async function summarizeSession(messages, options = {}) {
121
+ const { sessionKey = 'unknown' } = options;
122
+ const transcript = formatTranscript(messages, 12000);
123
+ if (!transcript) return null;
124
+
125
+ const buildPrompts = (strict = false) => {
126
+ const systemPrompt = `You are a session summarizer for an AI agent memory system. Your summaries help the agent recall past work in future sessions.
127
+
128
+ INSTRUCTIONS:
129
+ - Focus on OUTCOMES and DELIVERABLES, not conversational flow
130
+ - Use action verbs: implemented, fixed, configured, discovered, decided, explored
131
+ - Be specific: include file names, tool names, error messages, key decisions
132
+ - Write in the language the user used (Chinese if they spoke Chinese, English if English)
133
+
134
+ OUTPUT FORMAT: Return ONLY a valid JSON object with these fields:
135
+ {
136
+ "request": "What the user wanted to accomplish (1 sentence, specific)",
137
+ "investigated": "What was explored or researched to fulfill the request",
138
+ "learned": "Key technical insights, discoveries, or new understanding gained",
139
+ "completed": "Concrete deliverables: what was built, fixed, configured, or decided",
140
+ "next_steps": "Unfinished work or logical follow-up actions (null if fully completed)"
141
+ }
142
+
143
+ QUALITY GUIDELINES:
144
+ - "request" should capture the real goal, not just "user asked a question"
145
+ - "investigated" should list specific files read, APIs explored, architectures examined
146
+ - "learned" should contain reusable knowledge (not "learned how to do X" but the actual insight)
147
+ - "completed" should be a concrete outcome someone can verify
148
+ - "next_steps" should be actionable, not vague
149
+
150
+ ${strict ? 'CRITICAL: Output ONLY the JSON object. No markdown, no explanation, no code fences.' : ''}`;
151
+
152
+ const userPrompt = 'Session transcript:\n' + transcript + '\n\nJSON:';
153
+ return [
154
+ { role: 'system', content: systemPrompt },
155
+ { role: 'user', content: userPrompt }
156
+ ];
157
+ };
158
+
159
+ // First attempt
160
+ let content = await callGatewayChat(buildPrompts(false), { sessionKey, temperature: 0.2, max_tokens: 600 });
161
+ let parsed = parseSummaryJson(content || '');
162
+ if (parsed) return normalizeSummaryFields(parsed);
163
+
164
+ // Retry once with stricter instruction
165
+ content = await callGatewayChat(buildPrompts(true), { sessionKey, temperature: 0.1, max_tokens: 600 });
166
+ parsed = parseSummaryJson(content || '');
167
+ if (parsed) return normalizeSummaryFields(parsed);
168
+
169
+ return null;
170
+ }
171
+
172
+ // ============ Local Embedding Model (Qwen3-Embedding-0.6B) ============
173
+
174
+ const EMBEDDING_MODEL = 'Xenova/multilingual-e5-small';
175
+ const EMBEDDING_DIMS = 384;
176
+ const EMBEDDING_PREFIX = 'query: ';
177
+
178
+ // Singleton: lazily initialized embedding pipeline
179
+ let _extractorPromise = null;
180
+
181
+ function getExtractor() {
182
+ if (!_extractorPromise) {
183
+ _extractorPromise = (async () => {
184
+ try {
185
+ const { pipeline } = await import('@huggingface/transformers');
186
+ console.log('[openclaw-mem] Loading embedding model (first run downloads ~110MB)...');
187
+ const extractor = await pipeline('feature-extraction', EMBEDDING_MODEL);
188
+ console.log('[openclaw-mem] Embedding model loaded');
189
+ return extractor;
190
+ } catch (err) {
191
+ console.error('[openclaw-mem] Failed to load embedding model:', err.message);
192
+ _extractorPromise = null; // Allow retry
193
+ return null;
194
+ }
195
+ })();
196
+ }
197
+ return _extractorPromise;
198
+ }
199
+
200
+ /**
201
+ * Generate embedding vector for text using local Qwen3-Embedding-0.6B model.
202
+ * Returns Float32Array of 1024 dimensions, or null on failure.
203
+ */
204
+ export async function callGatewayEmbeddings(text) {
205
+ try {
206
+ const extractor = await getExtractor();
207
+ if (!extractor) return null;
208
+
209
+ const input = EMBEDDING_PREFIX + text;
210
+ const output = await extractor(input, {
211
+ pooling: 'mean',
212
+ normalize: true,
213
+ });
214
+
215
+ return new Float32Array(output.data);
216
+ } catch (err) {
217
+ console.error('[openclaw-mem] Embedding generation error:', err.message);
218
+ return null;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Generate embeddings for multiple texts sequentially.
224
+ * Returns array of Float32Array, or null entries on failure.
225
+ */
226
+ export async function batchEmbeddings(texts) {
227
+ const extractor = await getExtractor();
228
+ if (!extractor) return texts.map(() => null);
229
+
230
+ const results = [];
231
+ for (const text of texts) {
232
+ try {
233
+ const input = EMBEDDING_PREFIX + text;
234
+ const output = await extractor(input, {
235
+ pooling: 'mean',
236
+ normalize: true,
237
+ });
238
+ results.push(new Float32Array(output.data));
239
+ } catch (err) {
240
+ console.error('[openclaw-mem] Batch embedding error:', err.message);
241
+ results.push(null);
242
+ }
243
+ }
244
+ return results;
245
+ }
246
+
247
+ export { EMBEDDING_DIMS };
248
+
249
+ export const INTERNAL_SUMMARY_PREFIX = SUMMARY_SESSION_PREFIX;
250
+ export { callGatewayChat };