@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.
Files changed (86) hide show
  1. package/dist/capture/index.js +52 -8
  2. package/dist/capture/index.js.map +1 -1
  3. package/dist/ingest/chunker.d.ts +3 -4
  4. package/dist/ingest/chunker.d.ts.map +1 -1
  5. package/dist/ingest/chunker.js +19 -24
  6. package/dist/ingest/chunker.js.map +1 -1
  7. package/dist/ingest/providers/anthropic.d.ts +3 -1
  8. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  9. package/dist/ingest/providers/anthropic.js +79 -39
  10. package/dist/ingest/providers/anthropic.js.map +1 -1
  11. package/dist/ingest/providers/bedrock.d.ts +3 -1
  12. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  13. package/dist/ingest/providers/bedrock.js +79 -39
  14. package/dist/ingest/providers/bedrock.js.map +1 -1
  15. package/dist/ingest/providers/gemini.d.ts +3 -1
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +77 -39
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +3 -1
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +70 -30
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +3 -1
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +80 -39
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +1 -0
  28. package/dist/ingest/task-processor.d.ts.map +1 -1
  29. package/dist/ingest/task-processor.js +33 -9
  30. package/dist/ingest/task-processor.js.map +1 -1
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +29 -13
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/recall/engine.d.ts.map +1 -1
  35. package/dist/recall/engine.js +19 -14
  36. package/dist/recall/engine.js.map +1 -1
  37. package/dist/skill/bundled-memory-guide.d.ts +1 -5
  38. package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
  39. package/dist/skill/bundled-memory-guide.js +38 -97
  40. package/dist/skill/bundled-memory-guide.js.map +1 -1
  41. package/dist/skill/evaluator.js +1 -1
  42. package/dist/storage/sqlite.d.ts +1 -2
  43. package/dist/storage/sqlite.d.ts.map +1 -1
  44. package/dist/storage/sqlite.js +90 -17
  45. package/dist/storage/sqlite.js.map +1 -1
  46. package/dist/tools/memory-get.d.ts.map +1 -1
  47. package/dist/tools/memory-get.js +1 -3
  48. package/dist/tools/memory-get.js.map +1 -1
  49. package/dist/types.d.ts +2 -2
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/update-check.d.ts +21 -0
  54. package/dist/update-check.d.ts.map +1 -0
  55. package/dist/update-check.js +111 -0
  56. package/dist/update-check.js.map +1 -0
  57. package/dist/viewer/html.d.ts.map +1 -1
  58. package/dist/viewer/html.js +444 -182
  59. package/dist/viewer/html.js.map +1 -1
  60. package/dist/viewer/server.d.ts +1 -1
  61. package/dist/viewer/server.d.ts.map +1 -1
  62. package/dist/viewer/server.js +142 -78
  63. package/dist/viewer/server.js.map +1 -1
  64. package/index.ts +206 -198
  65. package/openclaw.plugin.json +3 -0
  66. package/package.json +5 -1
  67. package/scripts/postinstall.cjs +69 -2
  68. package/skill/memos-memory-guide/SKILL.md +73 -36
  69. package/src/capture/index.ts +52 -8
  70. package/src/ingest/chunker.ts +22 -30
  71. package/src/ingest/providers/anthropic.ts +89 -41
  72. package/src/ingest/providers/bedrock.ts +90 -41
  73. package/src/ingest/providers/gemini.ts +89 -41
  74. package/src/ingest/providers/index.ts +81 -35
  75. package/src/ingest/providers/openai.ts +90 -41
  76. package/src/ingest/task-processor.ts +29 -8
  77. package/src/ingest/worker.ts +31 -13
  78. package/src/recall/engine.ts +20 -13
  79. package/src/skill/bundled-memory-guide.ts +5 -96
  80. package/src/skill/evaluator.ts +1 -1
  81. package/src/storage/sqlite.ts +93 -21
  82. package/src/tools/memory-get.ts +1 -4
  83. package/src/types.ts +2 -9
  84. package/src/update-check.ts +96 -0
  85. package/src/viewer/html.ts +444 -182
  86. 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-fix ...`);
99
+ api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`);
138
100
 
139
- const { spawnSync } = require("child_process");
140
- const clearCache = () => {
141
- Object.keys(require.cache)
142
- .filter(k => k.includes("better-sqlite3") || k.includes("better_sqlite3"))
143
- .forEach(k => delete require.cache[k]);
144
- };
145
-
146
- const strategies = [
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
- for (const { label, cmd } of strategies) {
153
- if (sqliteReady) break;
154
- api.logger.info(`memos-local: trying ${label} ...`);
155
- try {
156
- const r = spawnSync(cmd[0], cmd[1], {
157
- cwd: pluginDir, stdio: "pipe", shell: true, timeout: 180_000,
158
- });
159
- const out = r.stdout?.toString()?.slice(0, 300) || "";
160
- const err = r.stderr?.toString()?.slice(0, 300) || "";
161
- if (out) api.logger.info(`memos-local: ${label} stdout: ${out}`);
162
- if (err && r.status !== 0) api.logger.warn(`memos-local: ${label} stderr: ${err}`);
163
- if (r.status === 0) {
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(`memos-local: ${label} exited with code ${r.status}`);
123
+ api.logger.warn("memos-local: rebuild exited 0 but module still not loadable from plugin dir");
171
124
  }
172
- } catch (e) {
173
- api.logger.warn(`memos-local: ${label} error: ${e}`);
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
- // Non-blocking update check
260
- checkForUpdate(api.logger, pluginDir).catch(() => {});
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
- const outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
280
- store.recordApiLog(toolName, inputParams, outputText, dur, ok);
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, narrow by role, or the auto-recalled context is insufficient.\n\n" +
295
- "Use role='user' to find what the user actually said.",
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: "Natural language search query" }),
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, maxResults, minScore, role } = params as {
304
- query: string;
305
- maxResults?: number;
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 = (params as any).agentId ?? "main";
292
+ const agentId = currentAgentId;
311
293
  const ownerFilter = [`agent:${agentId}`, "public"];
312
- const effectiveMaxResults = maxResults ?? 20;
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.length > 300
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 anchorChunk = store.getChunk(chunkId);
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.slice(0, DEFAULTS.excerptMaxChars),
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.slice(0, 150)}`)
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 chunk = store.getChunk(chunkId);
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.length > limit
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.slice(0, 200);
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 skillAgentId = (params as any).agentId ?? "main";
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.slice(0, 150)}${h.visibility === "public" ? " (public)" : ""}`,
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
- // Track recalled chunk IDs per turn to avoid re-storing them in agent_end
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 = (event as any).agentId ?? "main";
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 lastDoubleNewline = rawPrompt.lastIndexOf("\n\n");
927
- if (lastDoubleNewline > 0 && lastDoubleNewline < rawPrompt.length - 3) {
928
- const tail = rawPrompt.slice(lastDoubleNewline + 2).trim();
929
- if (tail.length >= 2) query = tail;
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: 20, minScore: 0.45, ownerFilter: recallOwnerFilter });
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 }, "no hits", dur, true);
947
- const noRecallHint =
948
- "## Memory system\n\nNo memories were automatically recalled for this turn (e.g. the user's message was long, vague, or no matching history). " +
949
- "You may still have relevant past context call the **memory_search** tool with a **short, focused query** you generate yourself " +
950
- "(e.g. key topics, names, or a rephrased question) to search the user's conversation history.";
951
- return { systemPrompt: noRecallHint };
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 systemACTION 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 }, `${result.hits.length} candidates (scores: ${result.hits.map(h => h.score.toFixed(3)).join(",")}) → 0 relevant`, dur, true);
977
- const noRecallHint =
978
- "## Memory system\n\nNo memories were automatically recalled for this turn (e.g. the user's message was long, vague, or no matching history). " +
979
- "You may still have relevant past context — call the **memory_search** tool with a **short, focused query** you generate yourself " +
980
- "(e.g. key topics, names, or a rephrased question) to search the user's conversation history.";
981
- return { systemPrompt: noRecallHint };
982
- }
983
- } else {
984
- // LLM filter unavailable (all models failed/timed out).
985
- // Fallback: only keep top candidates with score >= 0.6 (normalized),
986
- // capped at 5 to avoid flooding the context with noise.
987
- const FALLBACK_MIN_SCORE = 0.6;
988
- const FALLBACK_MAX = 5;
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.length > 300
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
- let tipsText = "";
1024
- if (!sufficient) {
1025
- const hasTask = filteredHits.some((h) => {
1026
- if (!h.taskId) return false;
1027
- const t = store.getTask(h.taskId);
1028
- return t && t.status !== "skipped";
1029
- });
1030
- const tips: string[] = [];
1031
- if (hasTask) {
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", { query }, context, recallDur, true);
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
- lastRecalledChunkIds = new Set(filteredHits.map(h => h.ref.chunkId));
1057
- lastRecalledSummaries = filteredHits.map(h => h.summary);
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
- systemPrompt: context,
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 = (event as any).agentId ?? "main";
1074
+ const captureAgentId = hookCtx?.agentId ?? "main";
1075
+ currentAgentId = captureAgentId;
1083
1076
  const captureOwner = `agent:${captureAgentId}`;
1084
- const sessionKey = (event as any).sessionKey ?? "default";
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
- text = stripInboundMetadata(text);
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(captured.length);
1197
+ telemetry.trackMemoryIngested(filteredCaptured.length);
1190
1198
  }
1191
1199
  } catch (err) {
1192
1200
  api.logger.warn(`memos-local: capture failed: ${String(err)}`);