@foxlight-foundation/foxmemory-plugin-v2 1.1.1 → 1.1.2
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/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/strip-openclaw-framing.d.ts +61 -0
- package/dist/strip-openclaw-framing.d.ts.map +1 -0
- package/{strip-openclaw-framing.ts → dist/strip-openclaw-framing.js} +81 -102
- package/dist/strip-openclaw-framing.js.map +1 -0
- package/package.json +1 -1
- package/.github/workflows/publish.yml +0 -33
- package/index.ts +0 -1546
- package/strip-openclaw-framing.test.ts +0 -188
- package/tsconfig.json +0 -20
package/index.ts
DELETED
|
@@ -1,1546 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw Memory (Mem0) Plugin
|
|
3
|
-
*
|
|
4
|
-
* Long-term memory via Mem0 — supports both the Mem0 platform
|
|
5
|
-
* and the open-source self-hosted SDK. Uses the official `mem0ai` package.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - 5 tools: memory_search, memory_list, memory_store, memory_get, memory_forget
|
|
9
|
-
* (with session/long-term scope support via scope and longTerm parameters)
|
|
10
|
-
* - Short-term (session-scoped) and long-term (user-scoped) memory
|
|
11
|
-
* - Auto-recall: injects relevant memories (both scopes) before each agent turn
|
|
12
|
-
* - Auto-capture: stores key facts scoped to the current session after each agent turn
|
|
13
|
-
* - CLI: openclaw mem0 search, openclaw mem0 stats
|
|
14
|
-
* - Dual mode: platform or open-source (self-hosted)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { Type } from "@sinclair/typebox";
|
|
18
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
19
|
-
import { stripOpenclawFraming } from "./strip-openclaw-framing";
|
|
20
|
-
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Types
|
|
23
|
-
// ============================================================================
|
|
24
|
-
|
|
25
|
-
type Mem0Mode = "platform" | "open-source";
|
|
26
|
-
|
|
27
|
-
type Mem0Config = {
|
|
28
|
-
mode: Mem0Mode;
|
|
29
|
-
// Platform-specific
|
|
30
|
-
apiKey?: string;
|
|
31
|
-
orgId?: string;
|
|
32
|
-
projectId?: string;
|
|
33
|
-
customInstructions: string;
|
|
34
|
-
customCategories: Record<string, string>;
|
|
35
|
-
enableGraph: boolean;
|
|
36
|
-
// OSS-specific
|
|
37
|
-
customPrompt?: string;
|
|
38
|
-
oss?: {
|
|
39
|
-
embedder?: { provider: string; config: Record<string, unknown> };
|
|
40
|
-
vectorStore?: { provider: string; config: Record<string, unknown> };
|
|
41
|
-
llm?: { provider: string; config: Record<string, unknown> };
|
|
42
|
-
historyDbPath?: string;
|
|
43
|
-
};
|
|
44
|
-
// Shared
|
|
45
|
-
userId: string;
|
|
46
|
-
baseUrl?: string;
|
|
47
|
-
requestTimeoutMs: number;
|
|
48
|
-
autoCapture: boolean;
|
|
49
|
-
autoRecall: boolean;
|
|
50
|
-
searchThreshold: number;
|
|
51
|
-
topK: number;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Unified types for the provider interface
|
|
55
|
-
interface AddOptions {
|
|
56
|
-
user_id: string;
|
|
57
|
-
run_id?: string;
|
|
58
|
-
custom_instructions?: string;
|
|
59
|
-
custom_categories?: Array<Record<string, string>>;
|
|
60
|
-
enable_graph?: boolean;
|
|
61
|
-
output_format?: string;
|
|
62
|
-
source?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface SearchOptions {
|
|
66
|
-
user_id: string;
|
|
67
|
-
run_id?: string;
|
|
68
|
-
top_k?: number;
|
|
69
|
-
threshold?: number;
|
|
70
|
-
limit?: number;
|
|
71
|
-
keyword_search?: boolean;
|
|
72
|
-
reranking?: boolean;
|
|
73
|
-
source?: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface ListOptions {
|
|
77
|
-
user_id: string;
|
|
78
|
-
run_id?: string;
|
|
79
|
-
page_size?: number;
|
|
80
|
-
source?: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface MemoryItem {
|
|
84
|
-
id: string;
|
|
85
|
-
memory: string;
|
|
86
|
-
user_id?: string;
|
|
87
|
-
score?: number;
|
|
88
|
-
categories?: string[];
|
|
89
|
-
metadata?: Record<string, unknown>;
|
|
90
|
-
created_at?: string;
|
|
91
|
-
updated_at?: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface AddResultItem {
|
|
95
|
-
id: string;
|
|
96
|
-
memory: string;
|
|
97
|
-
event: "ADD" | "UPDATE" | "DELETE" | "NOOP";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface AddResult {
|
|
101
|
-
results: AddResultItem[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// Unified Provider Interface
|
|
106
|
-
// ============================================================================
|
|
107
|
-
|
|
108
|
-
interface Mem0Provider {
|
|
109
|
-
add(
|
|
110
|
-
messages: Array<{ role: string; content: string }>,
|
|
111
|
-
options: AddOptions,
|
|
112
|
-
): Promise<AddResult>;
|
|
113
|
-
search(query: string, options: SearchOptions): Promise<MemoryItem[]>;
|
|
114
|
-
get(memoryId: string): Promise<MemoryItem>;
|
|
115
|
-
getAll(options: ListOptions): Promise<MemoryItem[]>;
|
|
116
|
-
delete(memoryId: string): Promise<void>;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// ============================================================================
|
|
121
|
-
// Foxmemory HTTP Provider (self-hosted API)
|
|
122
|
-
// ============================================================================
|
|
123
|
-
|
|
124
|
-
class FoxmemoryHttpProvider implements Mem0Provider {
|
|
125
|
-
constructor(
|
|
126
|
-
private readonly baseUrl: string,
|
|
127
|
-
private readonly timeoutMs: number,
|
|
128
|
-
) {}
|
|
129
|
-
|
|
130
|
-
private async post(path: string, body: unknown): Promise<any> {
|
|
131
|
-
const controller = new AbortController();
|
|
132
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
133
|
-
try {
|
|
134
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
135
|
-
method: "POST",
|
|
136
|
-
headers: { "content-type": "application/json" },
|
|
137
|
-
body: JSON.stringify(body),
|
|
138
|
-
signal: controller.signal,
|
|
139
|
-
});
|
|
140
|
-
const text = await res.text();
|
|
141
|
-
let json: any = null;
|
|
142
|
-
try { json = text ? JSON.parse(text) : null; } catch { json = { raw: text }; }
|
|
143
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} ${path}: ${json?.error?.message || json?.error || text}`);
|
|
144
|
-
return json;
|
|
145
|
-
} finally {
|
|
146
|
-
clearTimeout(timer);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private async getJson(path: string): Promise<any> {
|
|
151
|
-
const controller = new AbortController();
|
|
152
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
153
|
-
try {
|
|
154
|
-
const res = await fetch(`${this.baseUrl}${path}`, { signal: controller.signal });
|
|
155
|
-
const text = await res.text();
|
|
156
|
-
let json: any = null;
|
|
157
|
-
try { json = text ? JSON.parse(text) : null; } catch { json = { raw: text }; }
|
|
158
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} ${path}: ${json?.error?.message || json?.error || text}`);
|
|
159
|
-
return json;
|
|
160
|
-
} finally {
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private async deleteJson(path: string): Promise<any> {
|
|
166
|
-
const controller = new AbortController();
|
|
167
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
168
|
-
try {
|
|
169
|
-
const res = await fetch(`${this.baseUrl}${path}`, { method: "DELETE", signal: controller.signal });
|
|
170
|
-
const text = await res.text();
|
|
171
|
-
let json: any = null;
|
|
172
|
-
try { json = text ? JSON.parse(text) : null; } catch { json = { raw: text }; }
|
|
173
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} ${path}: ${json?.error?.message || json?.error || text}`);
|
|
174
|
-
return json;
|
|
175
|
-
} finally {
|
|
176
|
-
clearTimeout(timer);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async add(messages: Array<{ role: string; content: string }>, options: AddOptions): Promise<AddResult> {
|
|
181
|
-
const json = await this.post('/v2/memories', {
|
|
182
|
-
user_id: options.user_id,
|
|
183
|
-
run_id: options.run_id,
|
|
184
|
-
messages,
|
|
185
|
-
metadata: undefined,
|
|
186
|
-
infer_preferred: true,
|
|
187
|
-
fallback_raw: true,
|
|
188
|
-
});
|
|
189
|
-
const result = json?.data?.result || json?.result || { results: [] };
|
|
190
|
-
return normalizeAddResult(result);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
|
194
|
-
const json = await this.post('/v2/memories/search', {
|
|
195
|
-
query,
|
|
196
|
-
user_id: options.user_id,
|
|
197
|
-
run_id: options.run_id,
|
|
198
|
-
top_k: options.top_k ?? options.limit,
|
|
199
|
-
threshold: options.threshold,
|
|
200
|
-
keyword_search: options.keyword_search,
|
|
201
|
-
rerank: options.reranking,
|
|
202
|
-
source: options.source,
|
|
203
|
-
});
|
|
204
|
-
const rows = json?.data?.results || json?.results || [];
|
|
205
|
-
return normalizeSearchResults(rows);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async get(memoryId: string): Promise<MemoryItem> {
|
|
209
|
-
const json = await this.getJson(`/v2/memories/${encodeURIComponent(memoryId)}`);
|
|
210
|
-
return normalizeMemoryItem(json?.data || json);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
|
214
|
-
const json = await this.post('/v2/memories/list', {
|
|
215
|
-
filters: {
|
|
216
|
-
...(options.user_id ? { user_id: options.user_id } : {}),
|
|
217
|
-
...(options.run_id ? { run_id: options.run_id } : {}),
|
|
218
|
-
},
|
|
219
|
-
page_size: options.page_size,
|
|
220
|
-
});
|
|
221
|
-
const rows = json?.data || [];
|
|
222
|
-
return Array.isArray(rows) ? rows.map(normalizeMemoryItem) : [];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async delete(memoryId: string): Promise<void> {
|
|
226
|
-
await this.deleteJson(`/v2/memories/${encodeURIComponent(memoryId)}`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ============================================================================
|
|
231
|
-
// Platform Provider (Mem0 Cloud)
|
|
232
|
-
// ============================================================================
|
|
233
|
-
|
|
234
|
-
class PlatformProvider implements Mem0Provider {
|
|
235
|
-
private client: any; // MemoryClient from mem0ai
|
|
236
|
-
private initPromise: Promise<void> | null = null;
|
|
237
|
-
|
|
238
|
-
constructor(
|
|
239
|
-
private readonly apiKey: string,
|
|
240
|
-
private readonly orgId?: string,
|
|
241
|
-
private readonly projectId?: string,
|
|
242
|
-
) { }
|
|
243
|
-
|
|
244
|
-
private async ensureClient(): Promise<void> {
|
|
245
|
-
if (this.client) return;
|
|
246
|
-
if (this.initPromise) return this.initPromise;
|
|
247
|
-
this.initPromise = this._init();
|
|
248
|
-
return this.initPromise;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private async _init(): Promise<void> {
|
|
252
|
-
const { default: MemoryClient } = await import("mem0ai");
|
|
253
|
-
const opts: Record<string, string> = { apiKey: this.apiKey };
|
|
254
|
-
if (this.orgId) opts.org_id = this.orgId;
|
|
255
|
-
if (this.projectId) opts.project_id = this.projectId;
|
|
256
|
-
this.client = new MemoryClient(opts);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async add(
|
|
260
|
-
messages: Array<{ role: string; content: string }>,
|
|
261
|
-
options: AddOptions,
|
|
262
|
-
): Promise<AddResult> {
|
|
263
|
-
await this.ensureClient();
|
|
264
|
-
const opts: Record<string, unknown> = { user_id: options.user_id };
|
|
265
|
-
if (options.run_id) opts.run_id = options.run_id;
|
|
266
|
-
if (options.custom_instructions)
|
|
267
|
-
opts.custom_instructions = options.custom_instructions;
|
|
268
|
-
if (options.custom_categories)
|
|
269
|
-
opts.custom_categories = options.custom_categories;
|
|
270
|
-
if (options.enable_graph) opts.enable_graph = options.enable_graph;
|
|
271
|
-
if (options.output_format) opts.output_format = options.output_format;
|
|
272
|
-
if (options.source) opts.source = options.source;
|
|
273
|
-
|
|
274
|
-
const result = await this.client.add(messages, opts);
|
|
275
|
-
return normalizeAddResult(result);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
|
279
|
-
await this.ensureClient();
|
|
280
|
-
const opts: Record<string, unknown> = { user_id: options.user_id };
|
|
281
|
-
if (options.run_id) opts.run_id = options.run_id;
|
|
282
|
-
if (options.top_k != null) opts.top_k = options.top_k;
|
|
283
|
-
if (options.threshold != null) opts.threshold = options.threshold;
|
|
284
|
-
if (options.keyword_search != null) opts.keyword_search = options.keyword_search;
|
|
285
|
-
if (options.reranking != null) opts.reranking = options.reranking;
|
|
286
|
-
if (options.source) opts.source = options.source;
|
|
287
|
-
|
|
288
|
-
const results = await this.client.search(query, opts);
|
|
289
|
-
return normalizeSearchResults(results);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async get(memoryId: string): Promise<MemoryItem> {
|
|
293
|
-
await this.ensureClient();
|
|
294
|
-
const result = await this.client.get(memoryId);
|
|
295
|
-
return normalizeMemoryItem(result);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
|
299
|
-
await this.ensureClient();
|
|
300
|
-
const opts: Record<string, unknown> = { user_id: options.user_id };
|
|
301
|
-
if (options.run_id) opts.run_id = options.run_id;
|
|
302
|
-
if (options.page_size != null) opts.page_size = options.page_size;
|
|
303
|
-
if (options.source) opts.source = options.source;
|
|
304
|
-
|
|
305
|
-
const results = await this.client.getAll(opts);
|
|
306
|
-
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
|
|
307
|
-
// Some versions return { results: [...] }
|
|
308
|
-
if (results?.results && Array.isArray(results.results))
|
|
309
|
-
return results.results.map(normalizeMemoryItem);
|
|
310
|
-
return [];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async delete(memoryId: string): Promise<void> {
|
|
314
|
-
await this.ensureClient();
|
|
315
|
-
await this.client.delete(memoryId);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ============================================================================
|
|
320
|
-
// Open-Source Provider (Self-hosted)
|
|
321
|
-
// ============================================================================
|
|
322
|
-
|
|
323
|
-
class OSSProvider implements Mem0Provider {
|
|
324
|
-
private memory: any; // Memory from mem0ai/oss
|
|
325
|
-
private initPromise: Promise<void> | null = null;
|
|
326
|
-
|
|
327
|
-
constructor(
|
|
328
|
-
private readonly ossConfig?: Mem0Config["oss"],
|
|
329
|
-
private readonly customPrompt?: string,
|
|
330
|
-
private readonly resolvePath?: (p: string) => string,
|
|
331
|
-
) { }
|
|
332
|
-
|
|
333
|
-
private async ensureMemory(): Promise<void> {
|
|
334
|
-
if (this.memory) return;
|
|
335
|
-
if (this.initPromise) return this.initPromise;
|
|
336
|
-
this.initPromise = this._init();
|
|
337
|
-
return this.initPromise;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private async _init(): Promise<void> {
|
|
341
|
-
const { Memory } = await import("mem0ai/oss");
|
|
342
|
-
|
|
343
|
-
const config: Record<string, unknown> = { version: "v1.1" };
|
|
344
|
-
|
|
345
|
-
if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder;
|
|
346
|
-
if (this.ossConfig?.vectorStore)
|
|
347
|
-
config.vectorStore = this.ossConfig.vectorStore;
|
|
348
|
-
if (this.ossConfig?.llm) config.llm = this.ossConfig.llm;
|
|
349
|
-
|
|
350
|
-
if (this.ossConfig?.historyDbPath) {
|
|
351
|
-
const dbPath = this.resolvePath
|
|
352
|
-
? this.resolvePath(this.ossConfig.historyDbPath)
|
|
353
|
-
: this.ossConfig.historyDbPath;
|
|
354
|
-
config.historyDbPath = dbPath;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (this.customPrompt) config.customPrompt = this.customPrompt;
|
|
358
|
-
|
|
359
|
-
this.memory = new Memory(config);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async add(
|
|
363
|
-
messages: Array<{ role: string; content: string }>,
|
|
364
|
-
options: AddOptions,
|
|
365
|
-
): Promise<AddResult> {
|
|
366
|
-
await this.ensureMemory();
|
|
367
|
-
// OSS SDK uses camelCase: userId/runId, not user_id/run_id
|
|
368
|
-
const addOpts: Record<string, unknown> = { userId: options.user_id };
|
|
369
|
-
if (options.run_id) addOpts.runId = options.run_id;
|
|
370
|
-
if (options.source) addOpts.source = options.source;
|
|
371
|
-
const result = await this.memory.add(messages, addOpts);
|
|
372
|
-
return normalizeAddResult(result);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
|
376
|
-
await this.ensureMemory();
|
|
377
|
-
// OSS SDK uses camelCase: userId/runId, not user_id/run_id
|
|
378
|
-
const opts: Record<string, unknown> = { userId: options.user_id };
|
|
379
|
-
if (options.run_id) opts.runId = options.run_id;
|
|
380
|
-
if (options.limit != null) opts.limit = options.limit;
|
|
381
|
-
else if (options.top_k != null) opts.limit = options.top_k;
|
|
382
|
-
if (options.keyword_search != null) opts.keyword_search = options.keyword_search;
|
|
383
|
-
if (options.reranking != null) opts.reranking = options.reranking;
|
|
384
|
-
if (options.source) opts.source = options.source;
|
|
385
|
-
if (options.threshold != null) opts.threshold = options.threshold;
|
|
386
|
-
|
|
387
|
-
const results = await this.memory.search(query, opts);
|
|
388
|
-
const normalized = normalizeSearchResults(results);
|
|
389
|
-
|
|
390
|
-
// Filter results by threshold if specified (client-side filtering as fallback)
|
|
391
|
-
if (options.threshold != null) {
|
|
392
|
-
return normalized.filter(item => (item.score ?? 0) >= options.threshold!);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return normalized;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async get(memoryId: string): Promise<MemoryItem> {
|
|
399
|
-
await this.ensureMemory();
|
|
400
|
-
const result = await this.memory.get(memoryId);
|
|
401
|
-
return normalizeMemoryItem(result);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
|
405
|
-
await this.ensureMemory();
|
|
406
|
-
// OSS SDK uses camelCase: userId/runId, not user_id/run_id
|
|
407
|
-
const getAllOpts: Record<string, unknown> = { userId: options.user_id };
|
|
408
|
-
if (options.run_id) getAllOpts.runId = options.run_id;
|
|
409
|
-
if (options.source) getAllOpts.source = options.source;
|
|
410
|
-
const results = await this.memory.getAll(getAllOpts);
|
|
411
|
-
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
|
|
412
|
-
if (results?.results && Array.isArray(results.results))
|
|
413
|
-
return results.results.map(normalizeMemoryItem);
|
|
414
|
-
return [];
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async delete(memoryId: string): Promise<void> {
|
|
418
|
-
await this.ensureMemory();
|
|
419
|
-
await this.memory.delete(memoryId);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ============================================================================
|
|
424
|
-
// Result Normalizers
|
|
425
|
-
// ============================================================================
|
|
426
|
-
|
|
427
|
-
function normalizeMemoryItem(raw: any): MemoryItem {
|
|
428
|
-
return {
|
|
429
|
-
id: raw.id ?? raw.memory_id ?? "",
|
|
430
|
-
memory: raw.memory ?? raw.text ?? raw.content ?? "",
|
|
431
|
-
// Handle both platform (user_id, created_at) and OSS (userId, createdAt) field names
|
|
432
|
-
user_id: raw.user_id ?? raw.userId,
|
|
433
|
-
score: raw.score,
|
|
434
|
-
categories: raw.categories,
|
|
435
|
-
metadata: raw.metadata,
|
|
436
|
-
created_at: raw.created_at ?? raw.createdAt,
|
|
437
|
-
updated_at: raw.updated_at ?? raw.updatedAt,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function normalizeSearchResults(raw: any): MemoryItem[] {
|
|
442
|
-
// Platform API returns flat array, OSS returns { results: [...] }
|
|
443
|
-
if (Array.isArray(raw)) return raw.map(normalizeMemoryItem);
|
|
444
|
-
if (raw?.results && Array.isArray(raw.results))
|
|
445
|
-
return raw.results.map(normalizeMemoryItem);
|
|
446
|
-
return [];
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function normalizeAddResult(raw: any): AddResult {
|
|
450
|
-
// Handle { results: [...] } shape (both platform and OSS)
|
|
451
|
-
if (raw?.results && Array.isArray(raw.results)) {
|
|
452
|
-
return {
|
|
453
|
-
results: raw.results.map((r: any) => ({
|
|
454
|
-
id: r.id ?? r.memory_id ?? "",
|
|
455
|
-
memory: r.memory ?? r.text ?? "",
|
|
456
|
-
// Platform API may return PENDING status (async processing)
|
|
457
|
-
// OSS stores event in metadata.event
|
|
458
|
-
event: r.event ?? r.metadata?.event ?? (r.status === "PENDING" ? "ADD" : "ADD"),
|
|
459
|
-
})),
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
// Platform API without output_format returns flat array
|
|
463
|
-
if (Array.isArray(raw)) {
|
|
464
|
-
return {
|
|
465
|
-
results: raw.map((r: any) => ({
|
|
466
|
-
id: r.id ?? r.memory_id ?? "",
|
|
467
|
-
memory: r.memory ?? r.text ?? "",
|
|
468
|
-
event: r.event ?? r.metadata?.event ?? (r.status === "PENDING" ? "ADD" : "ADD"),
|
|
469
|
-
})),
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
return { results: [] };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ============================================================================
|
|
476
|
-
// Config Parser
|
|
477
|
-
// ============================================================================
|
|
478
|
-
|
|
479
|
-
function resolveEnvVars(value: string): string {
|
|
480
|
-
return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
|
|
481
|
-
const envValue = process.env[envVar];
|
|
482
|
-
if (!envValue) {
|
|
483
|
-
throw new Error(`Environment variable ${envVar} is not set`);
|
|
484
|
-
}
|
|
485
|
-
return envValue;
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {
|
|
490
|
-
const result: Record<string, unknown> = {};
|
|
491
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
492
|
-
if (typeof value === "string") {
|
|
493
|
-
result[key] = resolveEnvVars(value);
|
|
494
|
-
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
495
|
-
result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);
|
|
496
|
-
} else {
|
|
497
|
-
result[key] = value;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return result;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ============================================================================
|
|
504
|
-
// Default Custom Instructions & Categories
|
|
505
|
-
// ============================================================================
|
|
506
|
-
|
|
507
|
-
const DEFAULT_CUSTOM_INSTRUCTIONS = `Your Task: Extract and maintain a structured, evolving profile of the user from their conversations with an AI assistant. Capture information that would help the assistant provide personalized, context-aware responses in future interactions.
|
|
508
|
-
|
|
509
|
-
Information to Extract:
|
|
510
|
-
|
|
511
|
-
1. Identity & Demographics:
|
|
512
|
-
- Name, age, location, timezone, language preferences
|
|
513
|
-
- Occupation, employer, job role, industry
|
|
514
|
-
- Education background
|
|
515
|
-
|
|
516
|
-
2. Preferences & Opinions:
|
|
517
|
-
- Communication style preferences (formal/casual, verbose/concise)
|
|
518
|
-
- Tool and technology preferences (languages, frameworks, editors, OS)
|
|
519
|
-
- Content preferences (topics of interest, learning style)
|
|
520
|
-
- Strong opinions or values they've expressed
|
|
521
|
-
- Likes and dislikes they've explicitly stated
|
|
522
|
-
|
|
523
|
-
3. Goals & Projects:
|
|
524
|
-
- Current projects they're working on (name, description, status)
|
|
525
|
-
- Short-term and long-term goals
|
|
526
|
-
- Deadlines and milestones mentioned
|
|
527
|
-
- Problems they're actively trying to solve
|
|
528
|
-
|
|
529
|
-
4. Technical Context:
|
|
530
|
-
- Tech stack and tools they use
|
|
531
|
-
- Skill level in different areas (beginner/intermediate/expert)
|
|
532
|
-
- Development environment and setup details
|
|
533
|
-
- Recurring technical challenges
|
|
534
|
-
|
|
535
|
-
5. Relationships & People:
|
|
536
|
-
- Names and roles of people they mention (colleagues, family, friends)
|
|
537
|
-
- Team structure and dynamics
|
|
538
|
-
- Key contacts and their relevance
|
|
539
|
-
|
|
540
|
-
6. Decisions & Lessons:
|
|
541
|
-
- Important decisions made and their reasoning
|
|
542
|
-
- Lessons learned from past experiences
|
|
543
|
-
- Strategies that worked or failed
|
|
544
|
-
- Changed opinions or updated beliefs
|
|
545
|
-
|
|
546
|
-
7. Routines & Habits:
|
|
547
|
-
- Daily routines and schedules mentioned
|
|
548
|
-
- Work patterns (when they're productive, how they organize work)
|
|
549
|
-
- Health and wellness habits if voluntarily shared
|
|
550
|
-
|
|
551
|
-
8. Life Events:
|
|
552
|
-
- Significant events (new job, moving, milestones)
|
|
553
|
-
- Upcoming events or plans
|
|
554
|
-
- Changes in circumstances
|
|
555
|
-
|
|
556
|
-
Guidelines:
|
|
557
|
-
- Store memories as clear, self-contained statements (each memory should make sense on its own)
|
|
558
|
-
- Use third person: "User prefers..." not "I prefer..."
|
|
559
|
-
- Include temporal context when relevant: "As of [date], user is working on..."
|
|
560
|
-
- When information updates, UPDATE the existing memory rather than creating duplicates
|
|
561
|
-
- Merge related facts into single coherent memories when possible
|
|
562
|
-
- Preserve specificity: "User uses Next.js 14 with App Router" is better than "User uses React"
|
|
563
|
-
- Capture the WHY behind preferences when stated: "User prefers Vim because of keyboard-driven workflow"
|
|
564
|
-
|
|
565
|
-
Exclude:
|
|
566
|
-
- Passwords, API keys, tokens, or any authentication credentials
|
|
567
|
-
- Exact financial amounts (account balances, salaries) unless the user explicitly asks to remember them
|
|
568
|
-
- Temporary or ephemeral information (one-time questions, debugging sessions with no lasting insight)
|
|
569
|
-
- Generic small talk with no informational content
|
|
570
|
-
- The assistant's own responses unless they contain a commitment or promise to the user
|
|
571
|
-
- Raw code snippets (capture the intent/decision, not the code itself)
|
|
572
|
-
- Information the user explicitly asks not to remember`;
|
|
573
|
-
|
|
574
|
-
const DEFAULT_CUSTOM_CATEGORIES: Record<string, string> = {
|
|
575
|
-
identity:
|
|
576
|
-
"Personal identity information: name, age, location, timezone, occupation, employer, education, demographics",
|
|
577
|
-
preferences:
|
|
578
|
-
"Explicitly stated likes, dislikes, preferences, opinions, and values across any domain",
|
|
579
|
-
goals:
|
|
580
|
-
"Current and future goals, aspirations, objectives, targets the user is working toward",
|
|
581
|
-
projects:
|
|
582
|
-
"Specific projects, initiatives, or endeavors the user is working on, including status and details",
|
|
583
|
-
technical:
|
|
584
|
-
"Technical skills, tools, tech stack, development environment, programming languages, frameworks",
|
|
585
|
-
decisions:
|
|
586
|
-
"Important decisions made, reasoning behind choices, strategy changes, and their outcomes",
|
|
587
|
-
relationships:
|
|
588
|
-
"People mentioned by the user: colleagues, family, friends, their roles and relevance",
|
|
589
|
-
routines:
|
|
590
|
-
"Daily habits, work patterns, schedules, productivity routines, health and wellness habits",
|
|
591
|
-
life_events:
|
|
592
|
-
"Significant life events, milestones, transitions, upcoming plans and changes",
|
|
593
|
-
lessons:
|
|
594
|
-
"Lessons learned, insights gained, mistakes acknowledged, changed opinions or beliefs",
|
|
595
|
-
work:
|
|
596
|
-
"Work-related context: job responsibilities, workplace dynamics, career progression, professional challenges",
|
|
597
|
-
health:
|
|
598
|
-
"Health-related information voluntarily shared: conditions, medications, fitness, wellness goals",
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// ============================================================================
|
|
602
|
-
// Config Schema
|
|
603
|
-
// ============================================================================
|
|
604
|
-
|
|
605
|
-
const ALLOWED_KEYS = [
|
|
606
|
-
"mode",
|
|
607
|
-
"apiKey",
|
|
608
|
-
"userId",
|
|
609
|
-
"orgId",
|
|
610
|
-
"projectId",
|
|
611
|
-
"autoCapture",
|
|
612
|
-
"autoRecall",
|
|
613
|
-
"customInstructions",
|
|
614
|
-
"customCategories",
|
|
615
|
-
"customPrompt",
|
|
616
|
-
"enableGraph",
|
|
617
|
-
"searchThreshold",
|
|
618
|
-
"topK",
|
|
619
|
-
"oss",
|
|
620
|
-
"baseUrl",
|
|
621
|
-
"requestTimeoutMs",
|
|
622
|
-
];
|
|
623
|
-
|
|
624
|
-
function assertAllowedKeys(
|
|
625
|
-
value: Record<string, unknown>,
|
|
626
|
-
allowed: string[],
|
|
627
|
-
label: string,
|
|
628
|
-
) {
|
|
629
|
-
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
630
|
-
if (unknown.length === 0) return;
|
|
631
|
-
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const mem0ConfigSchema = {
|
|
635
|
-
parse(value: unknown): Mem0Config {
|
|
636
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
637
|
-
throw new Error("foxmemory-plugin-v2 config required");
|
|
638
|
-
}
|
|
639
|
-
const cfg = value as Record<string, unknown>;
|
|
640
|
-
assertAllowedKeys(cfg, ALLOWED_KEYS, "foxmemory-plugin-v2 config");
|
|
641
|
-
|
|
642
|
-
// Accept both "open-source" and legacy "oss" as open-source mode; everything else is platform
|
|
643
|
-
const mode: Mem0Mode =
|
|
644
|
-
cfg.mode === "oss" || cfg.mode === "open-source" ? "open-source" : "platform";
|
|
645
|
-
|
|
646
|
-
// Platform mode requires apiKey unless using baseUrl (foxmemory HTTP backend)
|
|
647
|
-
const hasBaseUrl = typeof cfg.baseUrl === "string" && cfg.baseUrl.length > 0;
|
|
648
|
-
if (mode === "platform" && !hasBaseUrl) {
|
|
649
|
-
if (typeof cfg.apiKey !== "string" || !cfg.apiKey) {
|
|
650
|
-
throw new Error(
|
|
651
|
-
"apiKey is required for platform mode (set mode: \"open-source\" for self-hosted)",
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Resolve env vars in oss config
|
|
657
|
-
let ossConfig: Mem0Config["oss"];
|
|
658
|
-
if (cfg.oss && typeof cfg.oss === "object" && !Array.isArray(cfg.oss)) {
|
|
659
|
-
ossConfig = resolveEnvVarsDeep(
|
|
660
|
-
cfg.oss as Record<string, unknown>,
|
|
661
|
-
) as unknown as Mem0Config["oss"];
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return {
|
|
665
|
-
mode,
|
|
666
|
-
apiKey:
|
|
667
|
-
typeof cfg.apiKey === "string" ? resolveEnvVars(cfg.apiKey) : undefined,
|
|
668
|
-
userId:
|
|
669
|
-
typeof cfg.userId === "string" && cfg.userId ? cfg.userId : "default",
|
|
670
|
-
baseUrl: typeof cfg.baseUrl === "string" ? resolveEnvVars(cfg.baseUrl).replace(/\/$/, "") : undefined,
|
|
671
|
-
requestTimeoutMs: typeof cfg.requestTimeoutMs === "number" ? cfg.requestTimeoutMs : 10000,
|
|
672
|
-
orgId: typeof cfg.orgId === "string" ? cfg.orgId : undefined,
|
|
673
|
-
projectId: typeof cfg.projectId === "string" ? cfg.projectId : undefined,
|
|
674
|
-
autoCapture: cfg.autoCapture !== false,
|
|
675
|
-
autoRecall: cfg.autoRecall !== false,
|
|
676
|
-
customInstructions:
|
|
677
|
-
typeof cfg.customInstructions === "string"
|
|
678
|
-
? cfg.customInstructions
|
|
679
|
-
: DEFAULT_CUSTOM_INSTRUCTIONS,
|
|
680
|
-
customCategories:
|
|
681
|
-
cfg.customCategories &&
|
|
682
|
-
typeof cfg.customCategories === "object" &&
|
|
683
|
-
!Array.isArray(cfg.customCategories)
|
|
684
|
-
? (cfg.customCategories as Record<string, string>)
|
|
685
|
-
: DEFAULT_CUSTOM_CATEGORIES,
|
|
686
|
-
customPrompt:
|
|
687
|
-
typeof cfg.customPrompt === "string"
|
|
688
|
-
? cfg.customPrompt
|
|
689
|
-
: DEFAULT_CUSTOM_INSTRUCTIONS,
|
|
690
|
-
enableGraph: cfg.enableGraph === true,
|
|
691
|
-
searchThreshold:
|
|
692
|
-
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
|
|
693
|
-
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
|
694
|
-
oss: ossConfig,
|
|
695
|
-
};
|
|
696
|
-
},
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
// ============================================================================
|
|
700
|
-
// Provider Factory
|
|
701
|
-
// ============================================================================
|
|
702
|
-
|
|
703
|
-
function createProvider(
|
|
704
|
-
cfg: Mem0Config,
|
|
705
|
-
api: OpenClawPluginApi,
|
|
706
|
-
): Mem0Provider {
|
|
707
|
-
if (cfg.baseUrl) {
|
|
708
|
-
return new FoxmemoryHttpProvider(cfg.baseUrl, cfg.requestTimeoutMs);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (cfg.mode === "open-source") {
|
|
712
|
-
return new OSSProvider(cfg.oss, cfg.customPrompt, (p) =>
|
|
713
|
-
api.resolvePath(p),
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// ============================================================================
|
|
721
|
-
// Helpers
|
|
722
|
-
// ============================================================================
|
|
723
|
-
|
|
724
|
-
/** Convert Record<string, string> categories to the array format mem0ai expects */
|
|
725
|
-
function categoriesToArray(
|
|
726
|
-
cats: Record<string, string>,
|
|
727
|
-
): Array<Record<string, string>> {
|
|
728
|
-
return Object.entries(cats).map(([key, value]) => ({ [key]: value }));
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// ============================================================================
|
|
732
|
-
// Plugin Definition
|
|
733
|
-
// ============================================================================
|
|
734
|
-
|
|
735
|
-
const memoryPlugin = {
|
|
736
|
-
id: "foxmemory-plugin-v2",
|
|
737
|
-
name: "Memory (Mem0)",
|
|
738
|
-
description:
|
|
739
|
-
"Mem0 memory backend — Mem0 platform or self-hosted open-source",
|
|
740
|
-
kind: "memory" as const,
|
|
741
|
-
configSchema: mem0ConfigSchema,
|
|
742
|
-
|
|
743
|
-
register(api: OpenClawPluginApi) {
|
|
744
|
-
const cfg = mem0ConfigSchema.parse(api.pluginConfig);
|
|
745
|
-
const provider = createProvider(cfg, api);
|
|
746
|
-
|
|
747
|
-
// Track current session ID for tool-level session scoping
|
|
748
|
-
let currentSessionId: string | undefined;
|
|
749
|
-
|
|
750
|
-
api.logger.info(
|
|
751
|
-
`foxmemory-plugin-v2: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
|
752
|
-
);
|
|
753
|
-
|
|
754
|
-
// Helper: build add options
|
|
755
|
-
function buildAddOptions(userIdOverride?: string, runId?: string): AddOptions {
|
|
756
|
-
const opts: AddOptions = {
|
|
757
|
-
user_id: userIdOverride || cfg.userId,
|
|
758
|
-
source: "OPENCLAW",
|
|
759
|
-
};
|
|
760
|
-
if (runId) opts.run_id = runId;
|
|
761
|
-
if (cfg.mode === "platform") {
|
|
762
|
-
opts.custom_instructions = cfg.customInstructions;
|
|
763
|
-
opts.custom_categories = categoriesToArray(cfg.customCategories);
|
|
764
|
-
opts.enable_graph = cfg.enableGraph;
|
|
765
|
-
opts.output_format = "v1.1";
|
|
766
|
-
}
|
|
767
|
-
return opts;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Helper: build search options
|
|
771
|
-
function buildSearchOptions(
|
|
772
|
-
userIdOverride?: string,
|
|
773
|
-
limit?: number,
|
|
774
|
-
runId?: string,
|
|
775
|
-
): SearchOptions {
|
|
776
|
-
const opts: SearchOptions = {
|
|
777
|
-
user_id: userIdOverride || cfg.userId,
|
|
778
|
-
top_k: limit ?? cfg.topK,
|
|
779
|
-
limit: limit ?? cfg.topK,
|
|
780
|
-
threshold: cfg.searchThreshold,
|
|
781
|
-
keyword_search: true,
|
|
782
|
-
reranking: true,
|
|
783
|
-
source: "OPENCLAW",
|
|
784
|
-
};
|
|
785
|
-
if (runId) opts.run_id = runId;
|
|
786
|
-
return opts;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ========================================================================
|
|
790
|
-
// Tools
|
|
791
|
-
// ========================================================================
|
|
792
|
-
|
|
793
|
-
api.registerTool(
|
|
794
|
-
{
|
|
795
|
-
name: "memory_search",
|
|
796
|
-
label: "Memory Search",
|
|
797
|
-
description:
|
|
798
|
-
"Search through long-term memories stored in Mem0. Use when you need context about user preferences, past decisions, or previously discussed topics.",
|
|
799
|
-
parameters: Type.Object({
|
|
800
|
-
query: Type.String({ description: "Search query" }),
|
|
801
|
-
limit: Type.Optional(
|
|
802
|
-
Type.Number({
|
|
803
|
-
description: `Max results (default: ${cfg.topK})`,
|
|
804
|
-
}),
|
|
805
|
-
),
|
|
806
|
-
userId: Type.Optional(
|
|
807
|
-
Type.String({
|
|
808
|
-
description:
|
|
809
|
-
"User ID to scope search (default: configured userId)",
|
|
810
|
-
}),
|
|
811
|
-
),
|
|
812
|
-
scope: Type.Optional(
|
|
813
|
-
Type.Union([
|
|
814
|
-
Type.Literal("session"),
|
|
815
|
-
Type.Literal("long-term"),
|
|
816
|
-
Type.Literal("all"),
|
|
817
|
-
], {
|
|
818
|
-
description:
|
|
819
|
-
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
|
820
|
-
}),
|
|
821
|
-
),
|
|
822
|
-
}),
|
|
823
|
-
async execute(_toolCallId, params) {
|
|
824
|
-
const { query, limit, userId, scope = "all" } = params as {
|
|
825
|
-
query: string;
|
|
826
|
-
limit?: number;
|
|
827
|
-
userId?: string;
|
|
828
|
-
scope?: "session" | "long-term" | "all";
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
try {
|
|
832
|
-
let results: MemoryItem[] = [];
|
|
833
|
-
|
|
834
|
-
if (scope === "session") {
|
|
835
|
-
if (currentSessionId) {
|
|
836
|
-
results = await provider.search(
|
|
837
|
-
query,
|
|
838
|
-
buildSearchOptions(userId, limit, currentSessionId),
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
} else if (scope === "long-term") {
|
|
842
|
-
results = await provider.search(
|
|
843
|
-
query,
|
|
844
|
-
buildSearchOptions(userId, limit),
|
|
845
|
-
);
|
|
846
|
-
} else {
|
|
847
|
-
// "all" — search both scopes and combine
|
|
848
|
-
const longTermResults = await provider.search(
|
|
849
|
-
query,
|
|
850
|
-
buildSearchOptions(userId, limit),
|
|
851
|
-
);
|
|
852
|
-
let sessionResults: MemoryItem[] = [];
|
|
853
|
-
if (currentSessionId) {
|
|
854
|
-
sessionResults = await provider.search(
|
|
855
|
-
query,
|
|
856
|
-
buildSearchOptions(userId, limit, currentSessionId),
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
// Deduplicate by ID, preferring long-term
|
|
860
|
-
const seen = new Set(longTermResults.map((r) => r.id));
|
|
861
|
-
results = [
|
|
862
|
-
...longTermResults,
|
|
863
|
-
...sessionResults.filter((r) => !seen.has(r.id)),
|
|
864
|
-
];
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
if (!results || results.length === 0) {
|
|
868
|
-
return {
|
|
869
|
-
content: [
|
|
870
|
-
{ type: "text", text: "No relevant memories found." },
|
|
871
|
-
],
|
|
872
|
-
details: { count: 0 },
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const text = results
|
|
877
|
-
.map(
|
|
878
|
-
(r, i) =>
|
|
879
|
-
`${i + 1}. ${r.memory} (score: ${((r.score ?? 0) * 100).toFixed(0)}%, id: ${r.id})`,
|
|
880
|
-
)
|
|
881
|
-
.join("\n");
|
|
882
|
-
|
|
883
|
-
const sanitized = results.map((r) => ({
|
|
884
|
-
id: r.id,
|
|
885
|
-
memory: r.memory,
|
|
886
|
-
score: r.score,
|
|
887
|
-
categories: r.categories,
|
|
888
|
-
created_at: r.created_at,
|
|
889
|
-
}));
|
|
890
|
-
|
|
891
|
-
return {
|
|
892
|
-
content: [
|
|
893
|
-
{
|
|
894
|
-
type: "text",
|
|
895
|
-
text: `Found ${results.length} memories:\n\n${text}`,
|
|
896
|
-
},
|
|
897
|
-
],
|
|
898
|
-
details: { count: results.length, memories: sanitized },
|
|
899
|
-
};
|
|
900
|
-
} catch (err) {
|
|
901
|
-
return {
|
|
902
|
-
content: [
|
|
903
|
-
{
|
|
904
|
-
type: "text",
|
|
905
|
-
text: `Memory search failed: ${String(err)}`,
|
|
906
|
-
},
|
|
907
|
-
],
|
|
908
|
-
details: { error: String(err) },
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
},
|
|
912
|
-
},
|
|
913
|
-
{ name: "memory_search" },
|
|
914
|
-
);
|
|
915
|
-
|
|
916
|
-
api.registerTool(
|
|
917
|
-
{
|
|
918
|
-
name: "memory_store",
|
|
919
|
-
label: "Memory Store",
|
|
920
|
-
description:
|
|
921
|
-
"Save important information in long-term memory via Mem0. Use for preferences, facts, decisions, and anything worth remembering.",
|
|
922
|
-
parameters: Type.Object({
|
|
923
|
-
text: Type.String({ description: "Information to remember" }),
|
|
924
|
-
userId: Type.Optional(
|
|
925
|
-
Type.String({
|
|
926
|
-
description: "User ID to scope this memory",
|
|
927
|
-
}),
|
|
928
|
-
),
|
|
929
|
-
metadata: Type.Optional(
|
|
930
|
-
Type.Record(Type.String(), Type.Unknown(), {
|
|
931
|
-
description: "Optional metadata to attach to this memory",
|
|
932
|
-
}),
|
|
933
|
-
),
|
|
934
|
-
longTerm: Type.Optional(
|
|
935
|
-
Type.Boolean({
|
|
936
|
-
description:
|
|
937
|
-
"Store as long-term (user-scoped) memory. Default: true. Set to false for session-scoped memory.",
|
|
938
|
-
}),
|
|
939
|
-
),
|
|
940
|
-
}),
|
|
941
|
-
async execute(_toolCallId, params) {
|
|
942
|
-
const { text, userId, longTerm = true } = params as {
|
|
943
|
-
text: string;
|
|
944
|
-
userId?: string;
|
|
945
|
-
metadata?: Record<string, unknown>;
|
|
946
|
-
longTerm?: boolean;
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
try {
|
|
950
|
-
// Strip any OpenClaw/FoxClaw framing that may have been quoted or
|
|
951
|
-
// copied into the explicit store text (metadata blocks, timestamps,
|
|
952
|
-
// directive tags). See strip-openclaw-framing.ts for details.
|
|
953
|
-
const cleanedText = stripOpenclawFraming(text);
|
|
954
|
-
if (!cleanedText) {
|
|
955
|
-
return {
|
|
956
|
-
content: [{ type: "text", text: "Nothing to store after stripping operational framing." }],
|
|
957
|
-
details: { action: "skipped" },
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const runId = !longTerm && currentSessionId ? currentSessionId : undefined;
|
|
962
|
-
const result = await provider.add(
|
|
963
|
-
[{ role: "user", content: cleanedText }],
|
|
964
|
-
buildAddOptions(userId, runId),
|
|
965
|
-
);
|
|
966
|
-
|
|
967
|
-
const added =
|
|
968
|
-
result.results?.filter((r) => r.event === "ADD") ?? [];
|
|
969
|
-
const updated =
|
|
970
|
-
result.results?.filter((r) => r.event === "UPDATE") ?? [];
|
|
971
|
-
|
|
972
|
-
const summary = [];
|
|
973
|
-
if (added.length > 0)
|
|
974
|
-
summary.push(
|
|
975
|
-
`${added.length} new memor${added.length === 1 ? "y" : "ies"} added`,
|
|
976
|
-
);
|
|
977
|
-
if (updated.length > 0)
|
|
978
|
-
summary.push(
|
|
979
|
-
`${updated.length} memor${updated.length === 1 ? "y" : "ies"} updated`,
|
|
980
|
-
);
|
|
981
|
-
if (summary.length === 0)
|
|
982
|
-
summary.push("No new memories extracted");
|
|
983
|
-
|
|
984
|
-
return {
|
|
985
|
-
content: [
|
|
986
|
-
{
|
|
987
|
-
type: "text",
|
|
988
|
-
text: `Stored: ${summary.join(", ")}. ${result.results?.map((r) => `[${r.event}] ${r.memory}`).join("; ") ?? ""}`,
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
details: {
|
|
992
|
-
action: "stored",
|
|
993
|
-
results: result.results,
|
|
994
|
-
},
|
|
995
|
-
};
|
|
996
|
-
} catch (err) {
|
|
997
|
-
return {
|
|
998
|
-
content: [
|
|
999
|
-
{
|
|
1000
|
-
type: "text",
|
|
1001
|
-
text: `Memory store failed: ${String(err)}`,
|
|
1002
|
-
},
|
|
1003
|
-
],
|
|
1004
|
-
details: { error: String(err) },
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
},
|
|
1008
|
-
},
|
|
1009
|
-
{ name: "memory_store" },
|
|
1010
|
-
);
|
|
1011
|
-
|
|
1012
|
-
api.registerTool(
|
|
1013
|
-
{
|
|
1014
|
-
name: "memory_get",
|
|
1015
|
-
label: "Memory Get",
|
|
1016
|
-
description: "Retrieve a specific memory by its ID from Mem0.",
|
|
1017
|
-
parameters: Type.Object({
|
|
1018
|
-
memoryId: Type.String({ description: "The memory ID to retrieve" }),
|
|
1019
|
-
}),
|
|
1020
|
-
async execute(_toolCallId, params) {
|
|
1021
|
-
const { memoryId } = params as { memoryId: string };
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
const memory = await provider.get(memoryId);
|
|
1025
|
-
|
|
1026
|
-
return {
|
|
1027
|
-
content: [
|
|
1028
|
-
{
|
|
1029
|
-
type: "text",
|
|
1030
|
-
text: `Memory ${memory.id}:\n${memory.memory}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}`,
|
|
1031
|
-
},
|
|
1032
|
-
],
|
|
1033
|
-
details: { memory },
|
|
1034
|
-
};
|
|
1035
|
-
} catch (err) {
|
|
1036
|
-
return {
|
|
1037
|
-
content: [
|
|
1038
|
-
{
|
|
1039
|
-
type: "text",
|
|
1040
|
-
text: `Memory get failed: ${String(err)}`,
|
|
1041
|
-
},
|
|
1042
|
-
],
|
|
1043
|
-
details: { error: String(err) },
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
},
|
|
1047
|
-
},
|
|
1048
|
-
{ name: "memory_get" },
|
|
1049
|
-
);
|
|
1050
|
-
|
|
1051
|
-
api.registerTool(
|
|
1052
|
-
{
|
|
1053
|
-
name: "memory_list",
|
|
1054
|
-
label: "Memory List",
|
|
1055
|
-
description:
|
|
1056
|
-
"List all stored memories for a user. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
|
|
1057
|
-
parameters: Type.Object({
|
|
1058
|
-
userId: Type.Optional(
|
|
1059
|
-
Type.String({
|
|
1060
|
-
description:
|
|
1061
|
-
"User ID to list memories for (default: configured userId)",
|
|
1062
|
-
}),
|
|
1063
|
-
),
|
|
1064
|
-
scope: Type.Optional(
|
|
1065
|
-
Type.Union([
|
|
1066
|
-
Type.Literal("session"),
|
|
1067
|
-
Type.Literal("long-term"),
|
|
1068
|
-
Type.Literal("all"),
|
|
1069
|
-
], {
|
|
1070
|
-
description:
|
|
1071
|
-
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
|
1072
|
-
}),
|
|
1073
|
-
),
|
|
1074
|
-
}),
|
|
1075
|
-
async execute(_toolCallId, params) {
|
|
1076
|
-
const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
|
|
1077
|
-
|
|
1078
|
-
try {
|
|
1079
|
-
let memories: MemoryItem[] = [];
|
|
1080
|
-
const uid = userId || cfg.userId;
|
|
1081
|
-
|
|
1082
|
-
if (scope === "session") {
|
|
1083
|
-
if (currentSessionId) {
|
|
1084
|
-
memories = await provider.getAll({
|
|
1085
|
-
user_id: uid,
|
|
1086
|
-
run_id: currentSessionId,
|
|
1087
|
-
source: "OPENCLAW",
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
} else if (scope === "long-term") {
|
|
1091
|
-
memories = await provider.getAll({ user_id: uid, source: "OPENCLAW" });
|
|
1092
|
-
} else {
|
|
1093
|
-
// "all" — combine both scopes
|
|
1094
|
-
const longTerm = await provider.getAll({ user_id: uid, source: "OPENCLAW" });
|
|
1095
|
-
let session: MemoryItem[] = [];
|
|
1096
|
-
if (currentSessionId) {
|
|
1097
|
-
session = await provider.getAll({
|
|
1098
|
-
user_id: uid,
|
|
1099
|
-
run_id: currentSessionId,
|
|
1100
|
-
source: "OPENCLAW",
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
const seen = new Set(longTerm.map((r) => r.id));
|
|
1104
|
-
memories = [
|
|
1105
|
-
...longTerm,
|
|
1106
|
-
...session.filter((r) => !seen.has(r.id)),
|
|
1107
|
-
];
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
if (!memories || memories.length === 0) {
|
|
1111
|
-
return {
|
|
1112
|
-
content: [
|
|
1113
|
-
{ type: "text", text: "No memories stored yet." },
|
|
1114
|
-
],
|
|
1115
|
-
details: { count: 0 },
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const text = memories
|
|
1120
|
-
.map(
|
|
1121
|
-
(r, i) =>
|
|
1122
|
-
`${i + 1}. ${r.memory} (id: ${r.id})`,
|
|
1123
|
-
)
|
|
1124
|
-
.join("\n");
|
|
1125
|
-
|
|
1126
|
-
const sanitized = memories.map((r) => ({
|
|
1127
|
-
id: r.id,
|
|
1128
|
-
memory: r.memory,
|
|
1129
|
-
categories: r.categories,
|
|
1130
|
-
created_at: r.created_at,
|
|
1131
|
-
}));
|
|
1132
|
-
|
|
1133
|
-
return {
|
|
1134
|
-
content: [
|
|
1135
|
-
{
|
|
1136
|
-
type: "text",
|
|
1137
|
-
text: `${memories.length} memories:\n\n${text}`,
|
|
1138
|
-
},
|
|
1139
|
-
],
|
|
1140
|
-
details: { count: memories.length, memories: sanitized },
|
|
1141
|
-
};
|
|
1142
|
-
} catch (err) {
|
|
1143
|
-
return {
|
|
1144
|
-
content: [
|
|
1145
|
-
{
|
|
1146
|
-
type: "text",
|
|
1147
|
-
text: `Memory list failed: ${String(err)}`,
|
|
1148
|
-
},
|
|
1149
|
-
],
|
|
1150
|
-
details: { error: String(err) },
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
},
|
|
1154
|
-
},
|
|
1155
|
-
{ name: "memory_list" },
|
|
1156
|
-
);
|
|
1157
|
-
|
|
1158
|
-
api.registerTool(
|
|
1159
|
-
{
|
|
1160
|
-
name: "memory_forget",
|
|
1161
|
-
label: "Memory Forget",
|
|
1162
|
-
description:
|
|
1163
|
-
"Delete memories from Mem0. Provide a specific memoryId to delete directly, or a query to search and delete matching memories. GDPR-compliant.",
|
|
1164
|
-
parameters: Type.Object({
|
|
1165
|
-
query: Type.Optional(
|
|
1166
|
-
Type.String({
|
|
1167
|
-
description: "Search query to find memory to delete",
|
|
1168
|
-
}),
|
|
1169
|
-
),
|
|
1170
|
-
memoryId: Type.Optional(
|
|
1171
|
-
Type.String({ description: "Specific memory ID to delete" }),
|
|
1172
|
-
),
|
|
1173
|
-
}),
|
|
1174
|
-
async execute(_toolCallId, params) {
|
|
1175
|
-
const { query, memoryId } = params as {
|
|
1176
|
-
query?: string;
|
|
1177
|
-
memoryId?: string;
|
|
1178
|
-
};
|
|
1179
|
-
|
|
1180
|
-
try {
|
|
1181
|
-
if (memoryId) {
|
|
1182
|
-
await provider.delete(memoryId);
|
|
1183
|
-
return {
|
|
1184
|
-
content: [
|
|
1185
|
-
{ type: "text", text: `Memory ${memoryId} forgotten.` },
|
|
1186
|
-
],
|
|
1187
|
-
details: { action: "deleted", id: memoryId },
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
if (query) {
|
|
1192
|
-
const results = await provider.search(
|
|
1193
|
-
query,
|
|
1194
|
-
buildSearchOptions(undefined, 5),
|
|
1195
|
-
);
|
|
1196
|
-
|
|
1197
|
-
if (!results || results.length === 0) {
|
|
1198
|
-
return {
|
|
1199
|
-
content: [
|
|
1200
|
-
{ type: "text", text: "No matching memories found." },
|
|
1201
|
-
],
|
|
1202
|
-
details: { found: 0 },
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// If single high-confidence match, delete directly
|
|
1207
|
-
if (
|
|
1208
|
-
results.length === 1 ||
|
|
1209
|
-
(results[0].score ?? 0) > 0.9
|
|
1210
|
-
) {
|
|
1211
|
-
await provider.delete(results[0].id);
|
|
1212
|
-
return {
|
|
1213
|
-
content: [
|
|
1214
|
-
{
|
|
1215
|
-
type: "text",
|
|
1216
|
-
text: `Forgotten: "${results[0].memory}"`,
|
|
1217
|
-
},
|
|
1218
|
-
],
|
|
1219
|
-
details: { action: "deleted", id: results[0].id },
|
|
1220
|
-
};
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
const list = results
|
|
1224
|
-
.map(
|
|
1225
|
-
(r) =>
|
|
1226
|
-
`- [${r.id}] ${r.memory.slice(0, 80)}${r.memory.length > 80 ? "..." : ""} (score: ${((r.score ?? 0) * 100).toFixed(0)}%)`,
|
|
1227
|
-
)
|
|
1228
|
-
.join("\n");
|
|
1229
|
-
|
|
1230
|
-
const candidates = results.map((r) => ({
|
|
1231
|
-
id: r.id,
|
|
1232
|
-
memory: r.memory,
|
|
1233
|
-
score: r.score,
|
|
1234
|
-
}));
|
|
1235
|
-
|
|
1236
|
-
return {
|
|
1237
|
-
content: [
|
|
1238
|
-
{
|
|
1239
|
-
type: "text",
|
|
1240
|
-
text: `Found ${results.length} candidates. Specify memoryId to delete:\n${list}`,
|
|
1241
|
-
},
|
|
1242
|
-
],
|
|
1243
|
-
details: { action: "candidates", candidates },
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
return {
|
|
1248
|
-
content: [
|
|
1249
|
-
{ type: "text", text: "Provide a query or memoryId." },
|
|
1250
|
-
],
|
|
1251
|
-
details: { error: "missing_param" },
|
|
1252
|
-
};
|
|
1253
|
-
} catch (err) {
|
|
1254
|
-
return {
|
|
1255
|
-
content: [
|
|
1256
|
-
{
|
|
1257
|
-
type: "text",
|
|
1258
|
-
text: `Memory forget failed: ${String(err)}`,
|
|
1259
|
-
},
|
|
1260
|
-
],
|
|
1261
|
-
details: { error: String(err) },
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
},
|
|
1265
|
-
},
|
|
1266
|
-
{ name: "memory_forget" },
|
|
1267
|
-
);
|
|
1268
|
-
|
|
1269
|
-
// ========================================================================
|
|
1270
|
-
// CLI Commands
|
|
1271
|
-
// ========================================================================
|
|
1272
|
-
|
|
1273
|
-
api.registerCli(
|
|
1274
|
-
({ program }) => {
|
|
1275
|
-
const mem0 = program
|
|
1276
|
-
.command("mem0")
|
|
1277
|
-
.description("Mem0 memory plugin commands");
|
|
1278
|
-
|
|
1279
|
-
mem0
|
|
1280
|
-
.command("search")
|
|
1281
|
-
.description("Search memories in Mem0")
|
|
1282
|
-
.argument("<query>", "Search query")
|
|
1283
|
-
.option("--limit <n>", "Max results", String(cfg.topK))
|
|
1284
|
-
.option("--scope <scope>", 'Memory scope: "session", "long-term", or "all"', "all")
|
|
1285
|
-
.action(async (query: string, opts: { limit: string; scope: string }) => {
|
|
1286
|
-
try {
|
|
1287
|
-
const limit = parseInt(opts.limit, 10);
|
|
1288
|
-
const scope = opts.scope as "session" | "long-term" | "all";
|
|
1289
|
-
|
|
1290
|
-
let allResults: MemoryItem[] = [];
|
|
1291
|
-
|
|
1292
|
-
if (scope === "session" || scope === "all") {
|
|
1293
|
-
if (currentSessionId) {
|
|
1294
|
-
const sessionResults = await provider.search(
|
|
1295
|
-
query,
|
|
1296
|
-
buildSearchOptions(undefined, limit, currentSessionId),
|
|
1297
|
-
);
|
|
1298
|
-
if (sessionResults?.length) {
|
|
1299
|
-
allResults.push(...sessionResults.map((r) => ({ ...r, _scope: "session" as const })));
|
|
1300
|
-
}
|
|
1301
|
-
} else if (scope === "session") {
|
|
1302
|
-
console.log("No active session ID available for session-scoped search.");
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
if (scope === "long-term" || scope === "all") {
|
|
1308
|
-
const longTermResults = await provider.search(
|
|
1309
|
-
query,
|
|
1310
|
-
buildSearchOptions(undefined, limit),
|
|
1311
|
-
);
|
|
1312
|
-
if (longTermResults?.length) {
|
|
1313
|
-
allResults.push(...longTermResults.map((r) => ({ ...r, _scope: "long-term" as const })));
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// Deduplicate by ID when searching "all"
|
|
1318
|
-
if (scope === "all") {
|
|
1319
|
-
const seen = new Set<string>();
|
|
1320
|
-
allResults = allResults.filter((r) => {
|
|
1321
|
-
if (seen.has(r.id)) return false;
|
|
1322
|
-
seen.add(r.id);
|
|
1323
|
-
return true;
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (!allResults.length) {
|
|
1328
|
-
console.log("No memories found.");
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
const output = allResults.map((r) => ({
|
|
1333
|
-
id: r.id,
|
|
1334
|
-
memory: r.memory,
|
|
1335
|
-
score: r.score,
|
|
1336
|
-
scope: (r as any)._scope,
|
|
1337
|
-
categories: r.categories,
|
|
1338
|
-
created_at: r.created_at,
|
|
1339
|
-
}));
|
|
1340
|
-
console.log(JSON.stringify(output, null, 2));
|
|
1341
|
-
} catch (err) {
|
|
1342
|
-
console.error(`Search failed: ${String(err)}`);
|
|
1343
|
-
}
|
|
1344
|
-
});
|
|
1345
|
-
|
|
1346
|
-
mem0
|
|
1347
|
-
.command("stats")
|
|
1348
|
-
.description("Show memory statistics from Mem0")
|
|
1349
|
-
.action(async () => {
|
|
1350
|
-
try {
|
|
1351
|
-
const memories = await provider.getAll({
|
|
1352
|
-
user_id: cfg.userId,
|
|
1353
|
-
source: "OPENCLAW",
|
|
1354
|
-
});
|
|
1355
|
-
console.log(`Mode: ${cfg.mode}`);
|
|
1356
|
-
console.log(`User: ${cfg.userId}`);
|
|
1357
|
-
console.log(
|
|
1358
|
-
`Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
|
|
1359
|
-
);
|
|
1360
|
-
console.log(`Graph enabled: ${cfg.enableGraph}`);
|
|
1361
|
-
console.log(
|
|
1362
|
-
`Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`,
|
|
1363
|
-
);
|
|
1364
|
-
} catch (err) {
|
|
1365
|
-
console.error(`Stats failed: ${String(err)}`);
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
},
|
|
1369
|
-
{ commands: ["mem0"] },
|
|
1370
|
-
);
|
|
1371
|
-
|
|
1372
|
-
// ========================================================================
|
|
1373
|
-
// Lifecycle Hooks
|
|
1374
|
-
// ========================================================================
|
|
1375
|
-
|
|
1376
|
-
// Auto-recall: inject relevant memories before agent starts
|
|
1377
|
-
if (cfg.autoRecall) {
|
|
1378
|
-
api.on("before_agent_start", async (event, ctx) => {
|
|
1379
|
-
if (!event.prompt || event.prompt.length < 5) return;
|
|
1380
|
-
|
|
1381
|
-
// Track session ID
|
|
1382
|
-
const sessionId = (ctx as any)?.sessionKey ?? undefined;
|
|
1383
|
-
if (sessionId) currentSessionId = sessionId;
|
|
1384
|
-
|
|
1385
|
-
try {
|
|
1386
|
-
// Search long-term memories (user-scoped)
|
|
1387
|
-
const longTermResults = await provider.search(
|
|
1388
|
-
event.prompt,
|
|
1389
|
-
buildSearchOptions(),
|
|
1390
|
-
);
|
|
1391
|
-
|
|
1392
|
-
// Search session memories (session-scoped) if we have a session ID
|
|
1393
|
-
let sessionResults: MemoryItem[] = [];
|
|
1394
|
-
if (currentSessionId) {
|
|
1395
|
-
sessionResults = await provider.search(
|
|
1396
|
-
event.prompt,
|
|
1397
|
-
buildSearchOptions(undefined, undefined, currentSessionId),
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Deduplicate session results against long-term
|
|
1402
|
-
const longTermIds = new Set(longTermResults.map((r) => r.id));
|
|
1403
|
-
const uniqueSessionResults = sessionResults.filter(
|
|
1404
|
-
(r) => !longTermIds.has(r.id),
|
|
1405
|
-
);
|
|
1406
|
-
|
|
1407
|
-
if (longTermResults.length === 0 && uniqueSessionResults.length === 0) return;
|
|
1408
|
-
|
|
1409
|
-
// Build context with clear labels
|
|
1410
|
-
let memoryContext = "";
|
|
1411
|
-
if (longTermResults.length > 0) {
|
|
1412
|
-
memoryContext += longTermResults
|
|
1413
|
-
.map(
|
|
1414
|
-
(r) =>
|
|
1415
|
-
`- ${r.memory}${r.categories?.length ? ` [${r.categories.join(", ")}]` : ""}`,
|
|
1416
|
-
)
|
|
1417
|
-
.join("\n");
|
|
1418
|
-
}
|
|
1419
|
-
if (uniqueSessionResults.length > 0) {
|
|
1420
|
-
if (memoryContext) memoryContext += "\n";
|
|
1421
|
-
memoryContext += "\nSession memories:\n";
|
|
1422
|
-
memoryContext += uniqueSessionResults
|
|
1423
|
-
.map((r) => `- ${r.memory}`)
|
|
1424
|
-
.join("\n");
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
const totalCount = longTermResults.length + uniqueSessionResults.length;
|
|
1428
|
-
api.logger.info(
|
|
1429
|
-
`foxmemory-plugin-v2: injecting ${totalCount} memories into context (${longTermResults.length} long-term, ${uniqueSessionResults.length} session)`,
|
|
1430
|
-
);
|
|
1431
|
-
|
|
1432
|
-
return {
|
|
1433
|
-
prependContext: `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
|
|
1434
|
-
};
|
|
1435
|
-
} catch (err) {
|
|
1436
|
-
api.logger.warn(`foxmemory-plugin-v2: recall failed: ${String(err)}`);
|
|
1437
|
-
}
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Auto-capture: store conversation context after agent ends
|
|
1442
|
-
if (cfg.autoCapture) {
|
|
1443
|
-
api.on("agent_end", async (event, ctx) => {
|
|
1444
|
-
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// Track session ID
|
|
1449
|
-
const sessionId = (ctx as any)?.sessionKey ?? undefined;
|
|
1450
|
-
if (sessionId) currentSessionId = sessionId;
|
|
1451
|
-
|
|
1452
|
-
try {
|
|
1453
|
-
// Extract messages, limiting to last 10
|
|
1454
|
-
const recentMessages = event.messages.slice(-10);
|
|
1455
|
-
const formattedMessages: Array<{
|
|
1456
|
-
role: string;
|
|
1457
|
-
content: string;
|
|
1458
|
-
}> = [];
|
|
1459
|
-
|
|
1460
|
-
for (const msg of recentMessages) {
|
|
1461
|
-
if (!msg || typeof msg !== "object") continue;
|
|
1462
|
-
const msgObj = msg as Record<string, unknown>;
|
|
1463
|
-
|
|
1464
|
-
const role = msgObj.role;
|
|
1465
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
1466
|
-
|
|
1467
|
-
let textContent = "";
|
|
1468
|
-
const content = msgObj.content;
|
|
1469
|
-
|
|
1470
|
-
if (typeof content === "string") {
|
|
1471
|
-
textContent = content;
|
|
1472
|
-
} else if (Array.isArray(content)) {
|
|
1473
|
-
for (const block of content) {
|
|
1474
|
-
if (
|
|
1475
|
-
block &&
|
|
1476
|
-
typeof block === "object" &&
|
|
1477
|
-
"text" in block &&
|
|
1478
|
-
typeof (block as Record<string, unknown>).text === "string"
|
|
1479
|
-
) {
|
|
1480
|
-
textContent +=
|
|
1481
|
-
(textContent ? "\n" : "") +
|
|
1482
|
-
((block as Record<string, unknown>).text as string);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
if (!textContent) continue;
|
|
1488
|
-
// Strip injected memory context, keep the actual user text
|
|
1489
|
-
if (textContent.includes("<relevant-memories>")) {
|
|
1490
|
-
textContent = textContent.replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>\s*/g, "").trim();
|
|
1491
|
-
if (!textContent) continue;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
// Strip OpenClaw/FoxClaw operational framing: inbound metadata blocks
|
|
1495
|
-
// (Sender, Conversation info, etc.), timestamp prefixes, and inline
|
|
1496
|
-
// directive tags ([[reply_to_current]], [[audio_as_voice]]). These are
|
|
1497
|
-
// gateway routing artifacts, not semantic content — if they leak into
|
|
1498
|
-
// memory extraction the LLM stores them as "facts."
|
|
1499
|
-
textContent = stripOpenclawFraming(textContent);
|
|
1500
|
-
if (!textContent) continue;
|
|
1501
|
-
|
|
1502
|
-
formattedMessages.push({
|
|
1503
|
-
role: role as string,
|
|
1504
|
-
content: textContent,
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
if (formattedMessages.length === 0) return;
|
|
1509
|
-
|
|
1510
|
-
const addOpts = buildAddOptions(undefined, currentSessionId);
|
|
1511
|
-
const result = await provider.add(
|
|
1512
|
-
formattedMessages,
|
|
1513
|
-
addOpts,
|
|
1514
|
-
);
|
|
1515
|
-
|
|
1516
|
-
const capturedCount = result.results?.length ?? 0;
|
|
1517
|
-
if (capturedCount > 0) {
|
|
1518
|
-
api.logger.info(
|
|
1519
|
-
`foxmemory-plugin-v2: auto-captured ${capturedCount} memories`,
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
} catch (err) {
|
|
1523
|
-
api.logger.warn(`foxmemory-plugin-v2: capture failed: ${String(err)}`);
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
// ========================================================================
|
|
1529
|
-
// Service
|
|
1530
|
-
// ========================================================================
|
|
1531
|
-
|
|
1532
|
-
api.registerService({
|
|
1533
|
-
id: "foxmemory-plugin-v2",
|
|
1534
|
-
start: () => {
|
|
1535
|
-
api.logger.info(
|
|
1536
|
-
`foxmemory-plugin-v2: initialized (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
|
1537
|
-
);
|
|
1538
|
-
},
|
|
1539
|
-
stop: () => {
|
|
1540
|
-
api.logger.info("foxmemory-plugin-v2: stopped");
|
|
1541
|
-
},
|
|
1542
|
-
});
|
|
1543
|
-
},
|
|
1544
|
-
};
|
|
1545
|
-
|
|
1546
|
-
export default memoryPlugin;
|