@memtensor/memos-local-openclaw-plugin 1.0.2-beta.5 → 1.0.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/capture/index.js +52 -8
- package/dist/capture/index.js.map +1 -1
- package/dist/ingest/chunker.d.ts +3 -4
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +19 -24
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts +3 -1
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +79 -39
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +3 -1
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +79 -39
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +3 -1
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +77 -39
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +3 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +70 -30
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +3 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +80 -39
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +1 -0
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +33 -9
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +29 -13
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +19 -14
- package/dist/recall/engine.js.map +1 -1
- package/dist/skill/bundled-memory-guide.d.ts +1 -5
- package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
- package/dist/skill/bundled-memory-guide.js +38 -97
- package/dist/skill/bundled-memory-guide.js.map +1 -1
- package/dist/skill/evaluator.js +1 -1
- package/dist/storage/sqlite.d.ts +1 -2
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +90 -17
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +1 -3
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts +21 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +111 -0
- package/dist/update-check.js.map +1 -0
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +444 -182
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +1 -1
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +142 -78
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +206 -198
- package/openclaw.plugin.json +3 -0
- package/package.json +5 -1
- package/scripts/postinstall.cjs +69 -2
- package/skill/memos-memory-guide/SKILL.md +73 -36
- package/src/capture/index.ts +52 -8
- package/src/ingest/chunker.ts +22 -30
- package/src/ingest/providers/anthropic.ts +89 -41
- package/src/ingest/providers/bedrock.ts +90 -41
- package/src/ingest/providers/gemini.ts +89 -41
- package/src/ingest/providers/index.ts +81 -35
- package/src/ingest/providers/openai.ts +90 -41
- package/src/ingest/task-processor.ts +29 -8
- package/src/ingest/worker.ts +31 -13
- package/src/recall/engine.ts +20 -13
- package/src/skill/bundled-memory-guide.ts +5 -96
- package/src/skill/evaluator.ts +1 -1
- package/src/storage/sqlite.ts +93 -21
- package/src/tools/memory-get.ts +1 -4
- package/src/types.ts +2 -9
- package/src/update-check.ts +96 -0
- package/src/viewer/html.ts +444 -182
- package/src/viewer/server.ts +101 -66
package/index.ts
CHANGED
|
@@ -23,44 +23,6 @@ import { Summarizer } from "./src/ingest/providers";
|
|
|
23
23
|
import { MEMORY_GUIDE_SKILL_MD } from "./src/skill/bundled-memory-guide";
|
|
24
24
|
import { Telemetry } from "./src/telemetry";
|
|
25
25
|
|
|
26
|
-
async function checkForUpdate(log: { info: (m: string) => void; warn: (m: string) => void }, pluginDir: string): Promise<void> {
|
|
27
|
-
try {
|
|
28
|
-
const pkgPath = path.join(pluginDir, "package.json");
|
|
29
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
30
|
-
const currentVersion = pkg.version;
|
|
31
|
-
const packageName = pkg.name;
|
|
32
|
-
if (!currentVersion || !packageName) return;
|
|
33
|
-
|
|
34
|
-
const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
35
|
-
signal: AbortSignal.timeout(8_000),
|
|
36
|
-
});
|
|
37
|
-
if (!resp.ok) return;
|
|
38
|
-
const data = await resp.json() as { version?: string };
|
|
39
|
-
const latestVersion = data.version;
|
|
40
|
-
if (!latestVersion) return;
|
|
41
|
-
|
|
42
|
-
if (latestVersion !== currentVersion) {
|
|
43
|
-
const msg = [
|
|
44
|
-
"",
|
|
45
|
-
"╔══════════════════════════════════════════════════════════════╗",
|
|
46
|
-
"║ MemOS Local Memory — New version available! ║",
|
|
47
|
-
"╠══════════════════════════════════════════════════════════════╣",
|
|
48
|
-
`║ Current: ${currentVersion.padEnd(12)} Latest: ${latestVersion.padEnd(13)} ║`,
|
|
49
|
-
"║ ║",
|
|
50
|
-
"║ Update: ║",
|
|
51
|
-
`║ openclaw plugins install ${packageName} ║`,
|
|
52
|
-
"║ ║",
|
|
53
|
-
"╚══════════════════════════════════════════════════════════════╝",
|
|
54
|
-
"",
|
|
55
|
-
].join("\n");
|
|
56
|
-
log.warn(`memos-local: ${msg}`);
|
|
57
|
-
} else {
|
|
58
|
-
log.info(`memos-local: version ${currentVersion} is up to date`);
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Silent fail — update check is best-effort
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
26
|
|
|
65
27
|
/** Remove near-duplicate hits based on summary word overlap (>70%). Keeps first (highest-scored) hit. */
|
|
66
28
|
function deduplicateHits<T extends { summary: string }>(hits: T[]): T[] {
|
|
@@ -134,44 +96,37 @@ const memosLocalPlugin = {
|
|
|
134
96
|
sqliteReady = trySqliteLoad();
|
|
135
97
|
|
|
136
98
|
if (!sqliteReady) {
|
|
137
|
-
api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-
|
|
99
|
+
api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`);
|
|
138
100
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
{ label: "npm rebuild better-sqlite3", cmd: ["npm", ["rebuild", "better-sqlite3"]] },
|
|
148
|
-
{ label: "npm install better-sqlite3 --no-save", cmd: ["npm", ["install", "better-sqlite3", "--no-save"]] },
|
|
149
|
-
{ label: "full npm install", cmd: ["npm", ["install", "--omit=dev"]] },
|
|
150
|
-
] as const;
|
|
101
|
+
try {
|
|
102
|
+
const { spawnSync } = require("child_process");
|
|
103
|
+
const rebuildResult = spawnSync("npm", ["rebuild", "better-sqlite3"], {
|
|
104
|
+
cwd: pluginDir,
|
|
105
|
+
stdio: "pipe",
|
|
106
|
+
shell: true,
|
|
107
|
+
timeout: 120_000,
|
|
108
|
+
});
|
|
151
109
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
api.logger.info(`memos-local:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
clearCache();
|
|
165
|
-
sqliteReady = trySqliteLoad();
|
|
166
|
-
if (sqliteReady) {
|
|
167
|
-
api.logger.info(`memos-local: better-sqlite3 fixed via "${label}"`);
|
|
168
|
-
}
|
|
110
|
+
const stdout = rebuildResult.stdout?.toString() || "";
|
|
111
|
+
const stderr = rebuildResult.stderr?.toString() || "";
|
|
112
|
+
if (stdout) api.logger.info(`memos-local: rebuild stdout: ${stdout.slice(0, 500)}`);
|
|
113
|
+
if (stderr) api.logger.warn(`memos-local: rebuild stderr: ${stderr.slice(0, 500)}`);
|
|
114
|
+
|
|
115
|
+
if (rebuildResult.status === 0) {
|
|
116
|
+
Object.keys(require.cache)
|
|
117
|
+
.filter(k => k.includes("better-sqlite3") || k.includes("better_sqlite3"))
|
|
118
|
+
.forEach(k => delete require.cache[k]);
|
|
119
|
+
sqliteReady = trySqliteLoad();
|
|
120
|
+
if (sqliteReady) {
|
|
121
|
+
api.logger.info("memos-local: better-sqlite3 auto-rebuild succeeded!");
|
|
169
122
|
} else {
|
|
170
|
-
api.logger.warn(
|
|
123
|
+
api.logger.warn("memos-local: rebuild exited 0 but module still not loadable from plugin dir");
|
|
171
124
|
}
|
|
172
|
-
}
|
|
173
|
-
api.logger.warn(`memos-local:
|
|
125
|
+
} else {
|
|
126
|
+
api.logger.warn(`memos-local: rebuild exited with code ${rebuildResult.status}`);
|
|
174
127
|
}
|
|
128
|
+
} catch (rebuildErr) {
|
|
129
|
+
api.logger.warn(`memos-local: auto-rebuild error: ${rebuildErr}`);
|
|
175
130
|
}
|
|
176
131
|
|
|
177
132
|
if (!sqliteReady) {
|
|
@@ -246,6 +201,29 @@ const memosLocalPlugin = {
|
|
|
246
201
|
ctx.log.warn(`memos-local: could not write to managed skills dir: ${e}`);
|
|
247
202
|
}
|
|
248
203
|
|
|
204
|
+
// Ensure plugin tools are enabled in openclaw.json tools.allow
|
|
205
|
+
try {
|
|
206
|
+
const openclawJsonPath = path.join(stateDir, "openclaw.json");
|
|
207
|
+
if (fs.existsSync(openclawJsonPath)) {
|
|
208
|
+
const raw = fs.readFileSync(openclawJsonPath, "utf-8");
|
|
209
|
+
const cfg = JSON.parse(raw);
|
|
210
|
+
const allow: string[] | undefined = cfg?.tools?.allow;
|
|
211
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")) {
|
|
212
|
+
const lastEntry = JSON.stringify(allow[allow.length - 1]);
|
|
213
|
+
const patched = raw.replace(
|
|
214
|
+
new RegExp(`(${lastEntry})(\\s*\\])`),
|
|
215
|
+
`$1,\n "group:plugins"$2`,
|
|
216
|
+
);
|
|
217
|
+
if (patched !== raw && patched.includes("group:plugins")) {
|
|
218
|
+
fs.writeFileSync(openclawJsonPath, patched, "utf-8");
|
|
219
|
+
ctx.log.info("memos-local: added 'group:plugins' to tools.allow in openclaw.json");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {
|
|
224
|
+
ctx.log.warn(`memos-local: could not patch tools.allow: ${e}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
249
227
|
worker.getTaskProcessor().onTaskCompleted((task) => {
|
|
250
228
|
skillEvolver.onTaskCompleted(task).catch((err) => {
|
|
251
229
|
ctx.log.warn(`SkillEvolver async error: ${err}`);
|
|
@@ -256,8 +234,9 @@ const memosLocalPlugin = {
|
|
|
256
234
|
|
|
257
235
|
api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
|
|
258
236
|
|
|
259
|
-
//
|
|
260
|
-
|
|
237
|
+
// Current agent ID — updated by hooks, read by tools for owner isolation.
|
|
238
|
+
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
239
|
+
let currentAgentId = "main";
|
|
261
240
|
|
|
262
241
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
263
242
|
async (...args: any[]) => {
|
|
@@ -276,8 +255,17 @@ const memosLocalPlugin = {
|
|
|
276
255
|
store.recordToolCall(toolName, dur, ok);
|
|
277
256
|
telemetry.trackToolCalled(toolName, dur, ok);
|
|
278
257
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
258
|
+
let outputText: string;
|
|
259
|
+
const det = result?.details;
|
|
260
|
+
if (det && Array.isArray(det.candidates)) {
|
|
261
|
+
outputText = JSON.stringify({
|
|
262
|
+
candidates: det.candidates,
|
|
263
|
+
filtered: det.hits ?? det.filtered ?? [],
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
|
|
267
|
+
}
|
|
268
|
+
store.recordApiLog(toolName, { ...inputParams, type: "tool_call" }, outputText, dur, ok);
|
|
281
269
|
} catch (_) { /* best-effort */ }
|
|
282
270
|
}
|
|
283
271
|
};
|
|
@@ -291,33 +279,35 @@ const memosLocalPlugin = {
|
|
|
291
279
|
description:
|
|
292
280
|
"Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
|
|
293
281
|
"Relevant memories are automatically injected at the start of each turn, but call this tool when you need " +
|
|
294
|
-
"to search with a different query
|
|
295
|
-
"
|
|
282
|
+
"to search with a different query or the auto-recalled context is insufficient. " +
|
|
283
|
+
"Pass only a short natural-language query (2-5 key words).",
|
|
296
284
|
parameters: Type.Object({
|
|
297
|
-
query: Type.String({ description: "
|
|
298
|
-
maxResults: Type.Optional(Type.Number({ description: "Max results (default 20, max 20)" })),
|
|
299
|
-
minScore: Type.Optional(Type.Number({ description: "Min score 0-1 (default 0.45, floor 0.35)" })),
|
|
300
|
-
role: Type.Optional(Type.String({ description: "Filter by role: 'user', 'assistant', or 'tool'. Use 'user' to find what the user said." })),
|
|
285
|
+
query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
|
|
301
286
|
}),
|
|
302
287
|
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
303
|
-
const { query
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
minScore?: number;
|
|
307
|
-
role?: string;
|
|
308
|
-
};
|
|
288
|
+
const { query } = params as { query: string };
|
|
289
|
+
const role = undefined;
|
|
290
|
+
const minScore = undefined;
|
|
309
291
|
|
|
310
|
-
const agentId =
|
|
292
|
+
const agentId = currentAgentId;
|
|
311
293
|
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
312
|
-
const effectiveMaxResults =
|
|
294
|
+
const effectiveMaxResults = 10;
|
|
313
295
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
314
296
|
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
315
297
|
ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
|
|
316
298
|
|
|
299
|
+
const rawCandidates = result.hits.map((h) => ({
|
|
300
|
+
chunkId: h.ref.chunkId,
|
|
301
|
+
role: h.source.role,
|
|
302
|
+
score: h.score,
|
|
303
|
+
summary: h.summary,
|
|
304
|
+
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
305
|
+
}));
|
|
306
|
+
|
|
317
307
|
if (result.hits.length === 0) {
|
|
318
308
|
return {
|
|
319
309
|
content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
|
|
320
|
-
details: { meta: result.meta },
|
|
310
|
+
details: { candidates: [], meta: result.meta },
|
|
321
311
|
};
|
|
322
312
|
}
|
|
323
313
|
|
|
@@ -327,8 +317,9 @@ const memosLocalPlugin = {
|
|
|
327
317
|
|
|
328
318
|
const candidates = result.hits.map((h, i) => ({
|
|
329
319
|
index: i + 1,
|
|
330
|
-
summary: h.summary,
|
|
331
320
|
role: h.source.role,
|
|
321
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
322
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
332
323
|
}));
|
|
333
324
|
|
|
334
325
|
const filterResult = await summarizer.filterRelevant(query, candidates);
|
|
@@ -341,7 +332,7 @@ const memosLocalPlugin = {
|
|
|
341
332
|
} else {
|
|
342
333
|
return {
|
|
343
334
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
344
|
-
details: { meta: result.meta },
|
|
335
|
+
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
345
336
|
};
|
|
346
337
|
}
|
|
347
338
|
}
|
|
@@ -349,7 +340,7 @@ const memosLocalPlugin = {
|
|
|
349
340
|
if (filteredHits.length === 0) {
|
|
350
341
|
return {
|
|
351
342
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
352
|
-
details: { meta: result.meta },
|
|
343
|
+
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
353
344
|
};
|
|
354
345
|
}
|
|
355
346
|
|
|
@@ -358,9 +349,7 @@ const memosLocalPlugin = {
|
|
|
358
349
|
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
359
350
|
|
|
360
351
|
const lines = filteredHits.map((h, i) => {
|
|
361
|
-
const excerpt = h.original_excerpt
|
|
362
|
-
? h.original_excerpt.slice(0, 297) + "..."
|
|
363
|
-
: h.original_excerpt;
|
|
352
|
+
const excerpt = h.original_excerpt;
|
|
364
353
|
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
365
354
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
366
355
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
@@ -401,6 +390,7 @@ const memosLocalPlugin = {
|
|
|
401
390
|
},
|
|
402
391
|
],
|
|
403
392
|
details: {
|
|
393
|
+
candidates: rawCandidates,
|
|
404
394
|
hits: filteredHits.map((h) => {
|
|
405
395
|
let effectiveTaskId = h.taskId;
|
|
406
396
|
if (effectiveTaskId) {
|
|
@@ -413,6 +403,8 @@ const memosLocalPlugin = {
|
|
|
413
403
|
skillId: h.skillId,
|
|
414
404
|
role: h.source.role,
|
|
415
405
|
score: h.score,
|
|
406
|
+
summary: h.summary,
|
|
407
|
+
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
416
408
|
};
|
|
417
409
|
}),
|
|
418
410
|
meta: result.meta,
|
|
@@ -437,13 +429,14 @@ const memosLocalPlugin = {
|
|
|
437
429
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
438
430
|
}),
|
|
439
431
|
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
440
|
-
ctx.log.debug(`memory_timeline called`);
|
|
432
|
+
ctx.log.debug(`memory_timeline called (agent=${currentAgentId})`);
|
|
441
433
|
const { chunkId, window: win } = params as {
|
|
442
434
|
chunkId: string;
|
|
443
435
|
window?: number;
|
|
444
436
|
};
|
|
445
437
|
|
|
446
|
-
const
|
|
438
|
+
const ownerFilter = [`agent:${currentAgentId}`, "public"];
|
|
439
|
+
const anchorChunk = store.getChunkForOwners(chunkId, ownerFilter);
|
|
447
440
|
if (!anchorChunk) {
|
|
448
441
|
return {
|
|
449
442
|
content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
|
|
@@ -452,7 +445,7 @@ const memosLocalPlugin = {
|
|
|
452
445
|
}
|
|
453
446
|
|
|
454
447
|
const w = win ?? DEFAULTS.timelineWindowDefault;
|
|
455
|
-
const neighbors = store.getNeighborChunks(anchorChunk.sessionKey, anchorChunk.turnId, anchorChunk.seq, w);
|
|
448
|
+
const neighbors = store.getNeighborChunks(anchorChunk.sessionKey, anchorChunk.turnId, anchorChunk.seq, w, ownerFilter);
|
|
456
449
|
const anchorTs = anchorChunk?.createdAt ?? 0;
|
|
457
450
|
|
|
458
451
|
const entries = neighbors.map((chunk) => {
|
|
@@ -463,14 +456,14 @@ const memosLocalPlugin = {
|
|
|
463
456
|
return {
|
|
464
457
|
relation,
|
|
465
458
|
role: chunk.role,
|
|
466
|
-
excerpt: chunk.content
|
|
459
|
+
excerpt: chunk.content,
|
|
467
460
|
ts: chunk.createdAt,
|
|
468
461
|
};
|
|
469
462
|
});
|
|
470
463
|
|
|
471
464
|
const rl = (r: string) => r === "user" ? "USER" : r === "assistant" ? "ASSISTANT" : r.toUpperCase();
|
|
472
465
|
const text = entries
|
|
473
|
-
.map((e) => `[${e.relation}] ${rl(e.role)}: ${e.excerpt
|
|
466
|
+
.map((e) => `[${e.relation}] ${rl(e.role)}: ${e.excerpt}`)
|
|
474
467
|
.join("\n");
|
|
475
468
|
|
|
476
469
|
return {
|
|
@@ -500,7 +493,8 @@ const memosLocalPlugin = {
|
|
|
500
493
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
501
494
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
502
495
|
|
|
503
|
-
const
|
|
496
|
+
const ownerFilter = [`agent:${currentAgentId}`, "public"];
|
|
497
|
+
const chunk = store.getChunkForOwners(chunkId, ownerFilter);
|
|
504
498
|
if (!chunk) {
|
|
505
499
|
return {
|
|
506
500
|
content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
|
|
@@ -508,9 +502,7 @@ const memosLocalPlugin = {
|
|
|
508
502
|
};
|
|
509
503
|
}
|
|
510
504
|
|
|
511
|
-
const content = chunk.content
|
|
512
|
-
? chunk.content.slice(0, limit) + "\u2026"
|
|
513
|
-
: chunk.content;
|
|
505
|
+
const content = chunk.content;
|
|
514
506
|
|
|
515
507
|
const who = chunk.role === "user" ? "USER said" : chunk.role === "assistant" ? "ASSISTANT replied" : chunk.role === "tool" ? "TOOL returned" : chunk.role.toUpperCase();
|
|
516
508
|
|
|
@@ -767,7 +759,7 @@ const memosLocalPlugin = {
|
|
|
767
759
|
const { v4: uuidv4 } = require("uuid");
|
|
768
760
|
const now = Date.now();
|
|
769
761
|
const chunkId = uuidv4();
|
|
770
|
-
const chunkSummary = writeSummary ?? writeContent
|
|
762
|
+
const chunkSummary = writeSummary ?? writeContent;
|
|
771
763
|
|
|
772
764
|
store.insertChunk({
|
|
773
765
|
id: chunkId,
|
|
@@ -824,8 +816,7 @@ const memosLocalPlugin = {
|
|
|
824
816
|
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
825
817
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
826
818
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
827
|
-
const
|
|
828
|
-
const currentOwner = `agent:${skillAgentId}`;
|
|
819
|
+
const currentOwner = `agent:${currentAgentId}`;
|
|
829
820
|
|
|
830
821
|
const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
|
|
831
822
|
|
|
@@ -837,7 +828,7 @@ const memosLocalPlugin = {
|
|
|
837
828
|
}
|
|
838
829
|
|
|
839
830
|
const text = hits.map((h, i) =>
|
|
840
|
-
`${i + 1}. [${h.name}] ${h.description
|
|
831
|
+
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (public)" : ""}`,
|
|
841
832
|
).join("\n");
|
|
842
833
|
|
|
843
834
|
return {
|
|
@@ -903,17 +894,13 @@ const memosLocalPlugin = {
|
|
|
903
894
|
|
|
904
895
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
905
896
|
|
|
906
|
-
|
|
907
|
-
let lastRecalledChunkIds: Set<string> = new Set();
|
|
908
|
-
let lastRecalledSummaries: string[] = [];
|
|
909
|
-
|
|
910
|
-
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[]; agentId?: string }) => {
|
|
911
|
-
lastRecalledChunkIds = new Set();
|
|
912
|
-
lastRecalledSummaries = [];
|
|
897
|
+
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
913
898
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
914
899
|
|
|
915
|
-
const recallAgentId =
|
|
900
|
+
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
901
|
+
currentAgentId = recallAgentId;
|
|
916
902
|
const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
|
|
903
|
+
ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
|
|
917
904
|
|
|
918
905
|
const recallT0 = performance.now();
|
|
919
906
|
let recallQuery = "";
|
|
@@ -923,10 +910,20 @@ const memosLocalPlugin = {
|
|
|
923
910
|
ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
|
|
924
911
|
|
|
925
912
|
let query = rawPrompt;
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
913
|
+
const senderTag = "Sender (untrusted metadata):";
|
|
914
|
+
const senderPos = rawPrompt.indexOf(senderTag);
|
|
915
|
+
if (senderPos !== -1) {
|
|
916
|
+
const afterSender = rawPrompt.slice(senderPos);
|
|
917
|
+
const fenceStart = afterSender.indexOf("```json");
|
|
918
|
+
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
919
|
+
if (fenceEnd > 0) {
|
|
920
|
+
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
921
|
+
} else {
|
|
922
|
+
const firstDblNl = afterSender.indexOf("\n\n");
|
|
923
|
+
if (firstDblNl > 0) {
|
|
924
|
+
query = afterSender.slice(firstDblNl + 2).trim();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
930
927
|
}
|
|
931
928
|
query = stripInboundMetadata(query);
|
|
932
929
|
query = query.replace(/<[^>]+>/g, "").trim();
|
|
@@ -938,25 +935,28 @@ const memosLocalPlugin = {
|
|
|
938
935
|
}
|
|
939
936
|
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
940
937
|
|
|
941
|
-
const result = await engine.search({ query, maxResults:
|
|
938
|
+
const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
942
939
|
if (result.hits.length === 0) {
|
|
943
940
|
ctx.log.debug("auto-recall: no candidates found");
|
|
944
941
|
const dur = performance.now() - recallT0;
|
|
945
942
|
store.recordToolCall("memory_search", dur, true);
|
|
946
|
-
store.recordApiLog("memory_search", { query },
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
943
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
|
|
944
|
+
if (query.length > 50) {
|
|
945
|
+
const noRecallHint =
|
|
946
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
947
|
+
"Auto-recall found no results for a long query. " +
|
|
948
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
949
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
950
|
+
return { prependContext: noRecallHint };
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
952
953
|
}
|
|
953
954
|
|
|
954
|
-
ctx.log.debug(`auto-recall: engine returned ${result.hits.length} hits (scores: ${result.hits.map(h => h.score.toFixed(3)).join(",")})`);
|
|
955
|
-
|
|
956
955
|
const candidates = result.hits.map((h, i) => ({
|
|
957
956
|
index: i + 1,
|
|
958
|
-
summary: h.summary,
|
|
959
957
|
role: h.source.role,
|
|
958
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
959
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
960
960
|
}));
|
|
961
961
|
|
|
962
962
|
let filteredHits = result.hits;
|
|
@@ -964,7 +964,6 @@ const memosLocalPlugin = {
|
|
|
964
964
|
|
|
965
965
|
const filterResult = await summarizer.filterRelevant(query, candidates);
|
|
966
966
|
if (filterResult !== null) {
|
|
967
|
-
ctx.log.debug(`auto-recall: LLM filter returned relevant=[${filterResult.relevant.join(",")}] sufficient=${filterResult.sufficient} (from ${candidates.length} candidates)`);
|
|
968
967
|
sufficient = filterResult.sufficient;
|
|
969
968
|
if (filterResult.relevant.length > 0) {
|
|
970
969
|
const indexSet = new Set(filterResult.relevant);
|
|
@@ -973,30 +972,19 @@ const memosLocalPlugin = {
|
|
|
973
972
|
ctx.log.debug("auto-recall: LLM filter returned no relevant hits");
|
|
974
973
|
const dur = performance.now() - recallT0;
|
|
975
974
|
store.recordToolCall("memory_search", dur, true);
|
|
976
|
-
store.recordApiLog("memory_search", { query },
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
filteredHits = result.hits.filter(h => h.score >= FALLBACK_MIN_SCORE).slice(0, FALLBACK_MAX);
|
|
990
|
-
ctx.log.warn(`auto-recall: LLM filter unavailable, fallback to top ${filteredHits.length} hits (score >= ${FALLBACK_MIN_SCORE})`);
|
|
991
|
-
if (filteredHits.length === 0) {
|
|
992
|
-
const dur = performance.now() - recallT0;
|
|
993
|
-
store.recordToolCall("memory_search", dur, true);
|
|
994
|
-
store.recordApiLog("memory_search", { query }, `${result.hits.length} candidates → LLM filter unavailable, no high-score fallback`, dur, true);
|
|
995
|
-
const noRecallHint =
|
|
996
|
-
"## Memory system\n\nNo memories were automatically recalled for this turn (e.g. the user's message was long, vague, or no matching history). " +
|
|
997
|
-
"You may still have relevant past context — call the **memory_search** tool with a **short, focused query** you generate yourself " +
|
|
998
|
-
"(e.g. key topics, names, or a rephrased question) to search the user's conversation history.";
|
|
999
|
-
return { systemPrompt: noRecallHint };
|
|
975
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
976
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
977
|
+
filtered: []
|
|
978
|
+
}), dur, true);
|
|
979
|
+
if (query.length > 50) {
|
|
980
|
+
const noRecallHint =
|
|
981
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
982
|
+
"Auto-recall found no relevant results for a long query. " +
|
|
983
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
984
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
985
|
+
return { prependContext: noRecallHint };
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
1000
988
|
}
|
|
1001
989
|
}
|
|
1002
990
|
|
|
@@ -1005,9 +993,7 @@ const memosLocalPlugin = {
|
|
|
1005
993
|
ctx.log.debug(`auto-recall: ${result.hits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
|
|
1006
994
|
|
|
1007
995
|
const lines = filteredHits.map((h, i) => {
|
|
1008
|
-
const excerpt = h.original_excerpt
|
|
1009
|
-
? h.original_excerpt.slice(0, 297) + "..."
|
|
1010
|
-
: h.original_excerpt;
|
|
996
|
+
const excerpt = h.original_excerpt;
|
|
1011
997
|
const parts: string[] = [`${i + 1}. [${h.source.role}]`];
|
|
1012
998
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
1013
999
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
@@ -1020,21 +1006,18 @@ const memosLocalPlugin = {
|
|
|
1020
1006
|
return parts.join("\n");
|
|
1021
1007
|
});
|
|
1022
1008
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
tips.push("→ call task_summary(taskId) for full task context");
|
|
1033
|
-
tips.push("→ call skill_get(taskId=...) if the task has a proven experience guide");
|
|
1034
|
-
}
|
|
1035
|
-
tips.push("→ call memory_timeline(chunkId) to expand surrounding conversation");
|
|
1036
|
-
tipsText = "\n\nIf more context is needed:\n" + tips.join("\n");
|
|
1009
|
+
const hasTask = filteredHits.some((h) => {
|
|
1010
|
+
if (!h.taskId) return false;
|
|
1011
|
+
const t = store.getTask(h.taskId);
|
|
1012
|
+
return t && t.status !== "skipped";
|
|
1013
|
+
});
|
|
1014
|
+
const tips: string[] = [];
|
|
1015
|
+
if (hasTask) {
|
|
1016
|
+
tips.push("- A hit has `task_id` → call `task_summary(taskId=\"...\")` to get the full task context (steps, code, results)");
|
|
1017
|
+
tips.push("- A task may have a reusable guide → call `skill_get(taskId=\"...\")` to retrieve the experience/skill");
|
|
1037
1018
|
}
|
|
1019
|
+
tips.push("- Need more surrounding dialogue → call `memory_timeline(chunkId=\"...\")` to expand context around a hit");
|
|
1020
|
+
const tipsText = "\n\nAvailable follow-up tools:\n" + tips.join("\n");
|
|
1038
1021
|
|
|
1039
1022
|
const contextParts = [
|
|
1040
1023
|
"## User's conversation history (from memory system)",
|
|
@@ -1050,19 +1033,28 @@ const memosLocalPlugin = {
|
|
|
1050
1033
|
|
|
1051
1034
|
const recallDur = performance.now() - recallT0;
|
|
1052
1035
|
store.recordToolCall("memory_search", recallDur, true);
|
|
1053
|
-
store.recordApiLog("memory_search", {
|
|
1036
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1037
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
1038
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
|
|
1039
|
+
}), recallDur, true);
|
|
1054
1040
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
1055
1041
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1042
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
|
|
1043
|
+
|
|
1044
|
+
if (!sufficient) {
|
|
1045
|
+
const searchHint =
|
|
1046
|
+
"\n\nIf these memories don't fully answer the question, " +
|
|
1047
|
+
"call `memory_search` with a shorter or rephrased query to find more.";
|
|
1048
|
+
return { prependContext: context + searchHint };
|
|
1049
|
+
}
|
|
1058
1050
|
|
|
1059
1051
|
return {
|
|
1060
|
-
|
|
1052
|
+
prependContext: context,
|
|
1061
1053
|
};
|
|
1062
1054
|
} catch (err) {
|
|
1063
1055
|
const dur = performance.now() - recallT0;
|
|
1064
1056
|
store.recordToolCall("memory_search", dur, false);
|
|
1065
|
-
try { store.recordApiLog("memory_search", { query: recallQuery }, `error: ${String(err)}`, dur, false); } catch (_) { /* best-effort */ }
|
|
1057
|
+
try { store.recordApiLog("memory_search", { type: "auto_recall", query: recallQuery }, `error: ${String(err)}`, dur, false); } catch (_) { /* best-effort */ }
|
|
1066
1058
|
ctx.log.warn(`auto-recall failed: ${String(err)}`);
|
|
1067
1059
|
}
|
|
1068
1060
|
});
|
|
@@ -1075,13 +1067,15 @@ const memosLocalPlugin = {
|
|
|
1075
1067
|
// already processed before the restart) and only capture future increments.
|
|
1076
1068
|
const sessionMsgCursor = new Map<string, number>();
|
|
1077
1069
|
|
|
1078
|
-
api.on("agent_end", async (event) => {
|
|
1070
|
+
api.on("agent_end", async (event: any, hookCtx?: { agentId?: string; sessionKey?: string; sessionId?: string }) => {
|
|
1079
1071
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
1080
1072
|
|
|
1081
1073
|
try {
|
|
1082
|
-
const captureAgentId =
|
|
1074
|
+
const captureAgentId = hookCtx?.agentId ?? "main";
|
|
1075
|
+
currentAgentId = captureAgentId;
|
|
1083
1076
|
const captureOwner = `agent:${captureAgentId}`;
|
|
1084
|
-
const sessionKey =
|
|
1077
|
+
const sessionKey = hookCtx?.sessionKey ?? "default";
|
|
1078
|
+
ctx.log.info(`agent_end: agentId=${captureAgentId} sessionKey=${sessionKey} (from hookCtx)`);
|
|
1085
1079
|
const cursorKey = `${sessionKey}::${captureAgentId}`;
|
|
1086
1080
|
const allMessages = event.messages;
|
|
1087
1081
|
|
|
@@ -1125,18 +1119,6 @@ const memosLocalPlugin = {
|
|
|
1125
1119
|
const b = block as Record<string, unknown>;
|
|
1126
1120
|
if (b.type === "text" && typeof b.text === "string") {
|
|
1127
1121
|
text += b.text + "\n";
|
|
1128
|
-
} else if (b.type === "tool_use" || b.type === "tool_call") {
|
|
1129
|
-
const toolName = (b.name ?? b.function ?? "") as string;
|
|
1130
|
-
const toolInput = b.input ?? b.arguments ?? {};
|
|
1131
|
-
const inputStr = typeof toolInput === "string" ? toolInput : JSON.stringify(toolInput, null, 2);
|
|
1132
|
-
const preview = inputStr.length > 500 ? inputStr.slice(0, 500) + "..." : inputStr;
|
|
1133
|
-
text += `[Tool Call: ${toolName}]\n${preview}\n\n`;
|
|
1134
|
-
} else if (b.type === "tool_result") {
|
|
1135
|
-
const toolContent = typeof b.content === "string" ? b.content
|
|
1136
|
-
: Array.isArray(b.content) ? (b.content as any[]).map((c: any) => c.text ?? "").join("\n")
|
|
1137
|
-
: JSON.stringify(b.content ?? "");
|
|
1138
|
-
const preview = toolContent.length > 800 ? toolContent.slice(0, 800) + "..." : toolContent;
|
|
1139
|
-
text += `[Tool Result]\n${preview}\n\n`;
|
|
1140
1122
|
} else if (typeof b.content === "string") {
|
|
1141
1123
|
text += b.content + "\n";
|
|
1142
1124
|
} else if (typeof b.text === "string") {
|
|
@@ -1148,8 +1130,37 @@ const memosLocalPlugin = {
|
|
|
1148
1130
|
text = text.trim();
|
|
1149
1131
|
if (!text) continue;
|
|
1150
1132
|
|
|
1133
|
+
// Strip injected <memory_context> prefix and OpenClaw metadata wrapper
|
|
1134
|
+
// to store only the user's actual input
|
|
1151
1135
|
if (role === "user") {
|
|
1152
|
-
|
|
1136
|
+
const mcTag = "<memory_context>";
|
|
1137
|
+
const mcEnd = "</memory_context>";
|
|
1138
|
+
const mcIdx = text.indexOf(mcTag);
|
|
1139
|
+
if (mcIdx !== -1) {
|
|
1140
|
+
const endIdx = text.indexOf(mcEnd);
|
|
1141
|
+
if (endIdx !== -1) {
|
|
1142
|
+
text = text.slice(endIdx + mcEnd.length).trim();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// Strip OpenClaw metadata envelope:
|
|
1146
|
+
// "Sender (untrusted metadata):\n```json\n{...}\n```\n\n[timestamp] actual message"
|
|
1147
|
+
const senderIdx = text.indexOf("Sender (untrusted metadata):");
|
|
1148
|
+
if (senderIdx !== -1) {
|
|
1149
|
+
const afterSender = text.slice(senderIdx);
|
|
1150
|
+
const fenceEnd = afterSender.indexOf("```\n", afterSender.indexOf("```json"));
|
|
1151
|
+
if (fenceEnd > 0) {
|
|
1152
|
+
const afterFence = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "");
|
|
1153
|
+
if (afterFence.trim().length >= 2) text = afterFence.trim();
|
|
1154
|
+
} else {
|
|
1155
|
+
const firstDblNl = afterSender.indexOf("\n\n");
|
|
1156
|
+
if (firstDblNl > 0) {
|
|
1157
|
+
const tail = afterSender.slice(firstDblNl + 2).trim();
|
|
1158
|
+
if (tail.length >= 2) text = tail;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Strip timestamp prefix like "[Thu 2026-03-05 15:23 GMT+8] "
|
|
1163
|
+
text = text.replace(/^\[.*?\]\s*/, "").trim();
|
|
1153
1164
|
if (!text) continue;
|
|
1154
1165
|
}
|
|
1155
1166
|
|
|
@@ -1181,12 +1192,9 @@ const memosLocalPlugin = {
|
|
|
1181
1192
|
const turnId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1182
1193
|
const captured = captureMessages(msgs, sessionKey, turnId, evidenceTag, ctx.log, captureOwner);
|
|
1183
1194
|
|
|
1184
|
-
lastRecalledChunkIds = new Set();
|
|
1185
|
-
lastRecalledSummaries = [];
|
|
1186
|
-
|
|
1187
1195
|
if (captured.length > 0) {
|
|
1188
1196
|
worker.enqueue(captured);
|
|
1189
|
-
telemetry.trackMemoryIngested(
|
|
1197
|
+
telemetry.trackMemoryIngested(filteredCaptured.length);
|
|
1190
1198
|
}
|
|
1191
1199
|
} catch (err) {
|
|
1192
1200
|
api.logger.warn(`memos-local: capture failed: ${String(err)}`);
|