@memtensor/memos-local-openclaw-plugin 1.0.8-beta.9 → 1.0.9-beta.1
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/index.ts +434 -334
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +59 -25
- package/src/client/hub.ts +11 -0
- package/src/hub/server.ts +13 -6
- package/src/ingest/providers/anthropic.ts +9 -6
- package/src/ingest/providers/bedrock.ts +9 -6
- package/src/ingest/providers/gemini.ts +9 -6
- package/src/ingest/providers/index.ts +123 -22
- package/src/ingest/providers/openai.ts +141 -6
- package/src/ingest/task-processor.ts +61 -41
- package/src/ingest/worker.ts +32 -11
- package/src/recall/engine.ts +2 -1
- package/src/sharing/types.ts +1 -0
- package/src/storage/sqlite.ts +194 -11
- package/src/types.ts +3 -0
- package/src/viewer/html.ts +954 -281
- package/src/viewer/server.ts +293 -20
- package/src/context-engine/index.ts +0 -321
package/index.ts
CHANGED
|
@@ -31,18 +31,6 @@ import { SkillInstaller } from "./src/skill/installer";
|
|
|
31
31
|
import { Summarizer } from "./src/ingest/providers";
|
|
32
32
|
import { MEMORY_GUIDE_SKILL_MD } from "./src/skill/bundled-memory-guide";
|
|
33
33
|
import { Telemetry } from "./src/telemetry";
|
|
34
|
-
import {
|
|
35
|
-
type AgentMessage as CEAgentMessage,
|
|
36
|
-
type PendingInjection,
|
|
37
|
-
deduplicateHits as ceDeduplicateHits,
|
|
38
|
-
formatMemoryBlock,
|
|
39
|
-
appendMemoryToMessage,
|
|
40
|
-
removeExistingMemoryBlock,
|
|
41
|
-
messageHasMemoryBlock,
|
|
42
|
-
getTextFromMessage,
|
|
43
|
-
insertSyntheticAssistantEntry,
|
|
44
|
-
findTargetAssistantEntry,
|
|
45
|
-
} from "./src/context-engine";
|
|
46
34
|
|
|
47
35
|
|
|
48
36
|
/** Remove near-duplicate hits based on summary word overlap (>70%). Keeps first (highest-scored) hit. */
|
|
@@ -65,6 +53,82 @@ function deduplicateHits<T extends { summary: string }>(hits: T[]): T[] {
|
|
|
65
53
|
return kept;
|
|
66
54
|
}
|
|
67
55
|
|
|
56
|
+
const NEW_SESSION_PROMPT_RE = /A new session was started via \/new or \/reset\./i;
|
|
57
|
+
const INTERNAL_CONTEXT_RE = /OpenClaw runtime context \(internal\):[\s\S]*/i;
|
|
58
|
+
const CONTINUE_PROMPT_RE = /^Continue where you left off\.[\s\S]*/i;
|
|
59
|
+
|
|
60
|
+
const buildMemoryPromptSection = ({ availableTools, citationsMode }: {
|
|
61
|
+
availableTools: Set<string>;
|
|
62
|
+
citationsMode?: string;
|
|
63
|
+
}) => {
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
const hasMemorySearch = availableTools.has("memory_search");
|
|
66
|
+
const hasMemoryGet = availableTools.has("memory_get");
|
|
67
|
+
|
|
68
|
+
if (!hasMemorySearch && !hasMemoryGet) {
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push("## Memory Recall");
|
|
73
|
+
lines.push(
|
|
74
|
+
"This workspace uses MemOS Local as the active memory slot. Prefer recalled memories and the memory tools before claiming prior context is unavailable.",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (hasMemorySearch && hasMemoryGet) {
|
|
78
|
+
lines.push(
|
|
79
|
+
"Use `memory_search` to locate relevant memories, then `memory_get` or `memory_timeline` when you need the full source text or surrounding context.",
|
|
80
|
+
);
|
|
81
|
+
} else if (hasMemorySearch) {
|
|
82
|
+
lines.push("Use `memory_search` before answering questions about prior conversations, preferences, plans, or decisions.");
|
|
83
|
+
} else {
|
|
84
|
+
lines.push("Use `memory_get` or `memory_timeline` to inspect the referenced memory before answering.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (citationsMode === "off") {
|
|
88
|
+
lines.push("Citations are disabled, so avoid mentioning internal memory ids unless the user asks.");
|
|
89
|
+
} else {
|
|
90
|
+
lines.push("When it helps the user verify a memory-backed claim, mention the relevant memory identifier or tool result.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push("");
|
|
94
|
+
return lines;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function normalizeAutoRecallQuery(rawPrompt: string): string {
|
|
98
|
+
let query = rawPrompt.trim();
|
|
99
|
+
|
|
100
|
+
const senderTag = "Sender (untrusted metadata):";
|
|
101
|
+
const senderPos = query.indexOf(senderTag);
|
|
102
|
+
if (senderPos !== -1) {
|
|
103
|
+
const afterSender = query.slice(senderPos);
|
|
104
|
+
const fenceStart = afterSender.indexOf("```json");
|
|
105
|
+
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
106
|
+
if (fenceEnd > 0) {
|
|
107
|
+
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
108
|
+
} else {
|
|
109
|
+
const firstDblNl = afterSender.indexOf("\n\n");
|
|
110
|
+
if (firstDblNl > 0) {
|
|
111
|
+
query = afterSender.slice(firstDblNl + 2).trim();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
query = stripInboundMetadata(query);
|
|
117
|
+
query = query.replace(/<[^>]+>/g, "").trim();
|
|
118
|
+
|
|
119
|
+
if (NEW_SESSION_PROMPT_RE.test(query)) {
|
|
120
|
+
query = query.replace(NEW_SESSION_PROMPT_RE, "").trim();
|
|
121
|
+
query = query.replace(/^(Execute|Run) your Session Startup sequence[^\n]*\n?/im, "").trim();
|
|
122
|
+
query = query.replace(/^Current time:[^\n]*(\n|$)/im, "").trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
query = query.replace(INTERNAL_CONTEXT_RE, "").trim();
|
|
126
|
+
query = query.replace(CONTINUE_PROMPT_RE, "").trim();
|
|
127
|
+
|
|
128
|
+
return query;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
68
132
|
const pluginConfigSchema = {
|
|
69
133
|
type: "object" as const,
|
|
70
134
|
additionalProperties: true,
|
|
@@ -96,6 +160,10 @@ const memosLocalPlugin = {
|
|
|
96
160
|
configSchema: pluginConfigSchema,
|
|
97
161
|
|
|
98
162
|
register(api: OpenClawPluginApi) {
|
|
163
|
+
api.registerMemoryCapability({
|
|
164
|
+
promptBuilder: buildMemoryPromptSection,
|
|
165
|
+
});
|
|
166
|
+
|
|
99
167
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
100
168
|
const localRequire = createRequire(import.meta.url);
|
|
101
169
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
@@ -290,7 +358,7 @@ const memosLocalPlugin = {
|
|
|
290
358
|
const raw = fs.readFileSync(openclawJsonPath, "utf-8");
|
|
291
359
|
const cfg = JSON.parse(raw);
|
|
292
360
|
const allow: string[] | undefined = cfg?.tools?.allow;
|
|
293
|
-
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")) {
|
|
361
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins") && !allow.includes("*")) {
|
|
294
362
|
const lastEntry = JSON.stringify(allow[allow.length - 1]);
|
|
295
363
|
const patched = raw.replace(
|
|
296
364
|
new RegExp(`(${lastEntry})(\\s*\\])`),
|
|
@@ -319,6 +387,7 @@ const memosLocalPlugin = {
|
|
|
319
387
|
// Current agent ID — updated by hooks, read by tools for owner isolation.
|
|
320
388
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
321
389
|
let currentAgentId = "main";
|
|
390
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
322
391
|
|
|
323
392
|
// ─── Check allowPromptInjection policy ───
|
|
324
393
|
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
@@ -332,214 +401,6 @@ const memosLocalPlugin = {
|
|
|
332
401
|
api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
|
|
333
402
|
}
|
|
334
403
|
|
|
335
|
-
// ─── Context Engine: inject memories into assistant messages ───
|
|
336
|
-
// Memories are wrapped in <relevant-memories> tags which OpenClaw's UI
|
|
337
|
-
// automatically strips from assistant messages, keeping the chat clean.
|
|
338
|
-
// Persisted to the session file so the prompt prefix stays stable for KV cache.
|
|
339
|
-
|
|
340
|
-
let pendingInjection: PendingInjection | null = null;
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
api.registerContextEngine("memos-local-openclaw-plugin", () => ({
|
|
344
|
-
info: {
|
|
345
|
-
id: "memos-local-openclaw-plugin",
|
|
346
|
-
name: "MemOS Local Memory Context Engine",
|
|
347
|
-
version: "1.0.0",
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
async ingest() {
|
|
351
|
-
return { ingested: false };
|
|
352
|
-
},
|
|
353
|
-
|
|
354
|
-
async assemble(params: {
|
|
355
|
-
sessionId: string;
|
|
356
|
-
sessionKey?: string;
|
|
357
|
-
messages: CEAgentMessage[];
|
|
358
|
-
tokenBudget?: number;
|
|
359
|
-
model?: string;
|
|
360
|
-
prompt?: string;
|
|
361
|
-
}) {
|
|
362
|
-
const { messages, prompt, sessionId, sessionKey } = params;
|
|
363
|
-
|
|
364
|
-
if (!allowPromptInjection || !prompt || prompt.length < 3) {
|
|
365
|
-
return { messages, estimatedTokens: 0 };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const recallT0 = performance.now();
|
|
369
|
-
try {
|
|
370
|
-
let query = prompt;
|
|
371
|
-
const senderTag = "Sender (untrusted metadata):";
|
|
372
|
-
const senderPos = query.indexOf(senderTag);
|
|
373
|
-
if (senderPos !== -1) {
|
|
374
|
-
const afterSender = query.slice(senderPos);
|
|
375
|
-
const fenceStart = afterSender.indexOf("```json");
|
|
376
|
-
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
377
|
-
if (fenceEnd > 0) {
|
|
378
|
-
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
379
|
-
} else {
|
|
380
|
-
const firstDblNl = afterSender.indexOf("\n\n");
|
|
381
|
-
if (firstDblNl > 0) {
|
|
382
|
-
query = afterSender.slice(firstDblNl + 2).trim();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
query = stripInboundMetadata(query).replace(/<[^>]+>/g, "").trim();
|
|
387
|
-
|
|
388
|
-
if (query.length < 2) {
|
|
389
|
-
return { messages, estimatedTokens: 0 };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
ctx.log.debug(`context-engine assemble: query="${query.slice(0, 80)}"`);
|
|
393
|
-
|
|
394
|
-
const recallOwner = [`agent:${currentAgentId}`, "public"];
|
|
395
|
-
const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwner });
|
|
396
|
-
const filteredHits = ceDeduplicateHits(
|
|
397
|
-
result.hits.filter((h: SearchHit) => h.score >= 0.5),
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
if (filteredHits.length === 0) {
|
|
401
|
-
ctx.log.debug("context-engine assemble: no memory hits");
|
|
402
|
-
return { messages, estimatedTokens: 0 };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const memoryBlock = formatMemoryBlock(filteredHits);
|
|
406
|
-
const cloned: CEAgentMessage[] = messages.map((m) => structuredClone(m));
|
|
407
|
-
|
|
408
|
-
let lastAssistantIdx = -1;
|
|
409
|
-
for (let i = cloned.length - 1; i >= 0; i--) {
|
|
410
|
-
if (cloned[i].role === "assistant") {
|
|
411
|
-
lastAssistantIdx = i;
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const sk = sessionKey ?? sessionId;
|
|
417
|
-
|
|
418
|
-
if (lastAssistantIdx < 0) {
|
|
419
|
-
const syntheticAssistant: CEAgentMessage = {
|
|
420
|
-
role: "assistant",
|
|
421
|
-
content: [{ type: "text", text: memoryBlock }],
|
|
422
|
-
timestamp: Date.now(),
|
|
423
|
-
stopReason: "end_turn",
|
|
424
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
|
|
425
|
-
};
|
|
426
|
-
pendingInjection = { sessionKey: sk, memoryBlock, isSynthetic: true };
|
|
427
|
-
ctx.log.info(`context-engine assemble: first turn, injecting synthetic assistant (${filteredHits.length} memories)`);
|
|
428
|
-
return { messages: [...cloned, syntheticAssistant], estimatedTokens: 0 };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
removeExistingMemoryBlock(cloned[lastAssistantIdx]);
|
|
432
|
-
appendMemoryToMessage(cloned[lastAssistantIdx], memoryBlock);
|
|
433
|
-
pendingInjection = { sessionKey: sk, memoryBlock, isSynthetic: false };
|
|
434
|
-
|
|
435
|
-
const dur = performance.now() - recallT0;
|
|
436
|
-
ctx.log.info(`context-engine assemble: injected ${filteredHits.length} memories into assistant[${lastAssistantIdx}] (${dur.toFixed(0)}ms)`);
|
|
437
|
-
return { messages: cloned, estimatedTokens: 0 };
|
|
438
|
-
} catch (err) {
|
|
439
|
-
ctx.log.warn(`context-engine assemble failed: ${err}`);
|
|
440
|
-
return { messages, estimatedTokens: 0 };
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
async afterTurn() {},
|
|
445
|
-
|
|
446
|
-
async compact(params: any) {
|
|
447
|
-
try {
|
|
448
|
-
const { delegateCompactionToRuntime } = await import("openclaw/plugin-sdk");
|
|
449
|
-
return await delegateCompactionToRuntime(params);
|
|
450
|
-
} catch {
|
|
451
|
-
return { ok: true, compacted: false, reason: "delegateCompactionToRuntime not available" };
|
|
452
|
-
}
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
async maintain(params: {
|
|
456
|
-
sessionId: string;
|
|
457
|
-
sessionKey?: string;
|
|
458
|
-
sessionFile: string;
|
|
459
|
-
runtimeContext?: { rewriteTranscriptEntries?: (req: any) => Promise<any> };
|
|
460
|
-
}) {
|
|
461
|
-
const noChange = { changed: false, bytesFreed: 0, rewrittenEntries: 0 };
|
|
462
|
-
|
|
463
|
-
if (!pendingInjection) return noChange;
|
|
464
|
-
|
|
465
|
-
const sk = params.sessionKey ?? params.sessionId;
|
|
466
|
-
if (pendingInjection.sessionKey !== sk) {
|
|
467
|
-
pendingInjection = null;
|
|
468
|
-
return { ...noChange, reason: "session mismatch" };
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
if (pendingInjection.isSynthetic) {
|
|
473
|
-
// First turn: INSERT synthetic assistant before existing entries
|
|
474
|
-
const { SessionManager } = await import("@mariozechner/pi-coding-agent");
|
|
475
|
-
const sm = SessionManager.open(params.sessionFile);
|
|
476
|
-
const ok = insertSyntheticAssistantEntry(sm, pendingInjection.memoryBlock);
|
|
477
|
-
pendingInjection = null;
|
|
478
|
-
if (ok) {
|
|
479
|
-
ctx.log.info("context-engine maintain: persisted synthetic assistant message");
|
|
480
|
-
return { changed: true, bytesFreed: 0, rewrittenEntries: 1 };
|
|
481
|
-
}
|
|
482
|
-
return { ...noChange, reason: "empty branch, could not insert synthetic" };
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Subsequent turns: REPLACE last assistant entry with memory-injected version
|
|
486
|
-
if (!params.runtimeContext?.rewriteTranscriptEntries) {
|
|
487
|
-
pendingInjection = null;
|
|
488
|
-
return { ...noChange, reason: "rewriteTranscriptEntries not available" };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const { SessionManager } = await import("@mariozechner/pi-coding-agent");
|
|
492
|
-
const sm = SessionManager.open(params.sessionFile);
|
|
493
|
-
const branch = sm.getBranch();
|
|
494
|
-
const targetEntry = findTargetAssistantEntry(branch);
|
|
495
|
-
|
|
496
|
-
if (!targetEntry) {
|
|
497
|
-
pendingInjection = null;
|
|
498
|
-
return { ...noChange, reason: "no target assistant entry found" };
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const modifiedMessage = structuredClone(targetEntry.message!);
|
|
502
|
-
removeExistingMemoryBlock(modifiedMessage as CEAgentMessage);
|
|
503
|
-
appendMemoryToMessage(modifiedMessage as CEAgentMessage, pendingInjection.memoryBlock);
|
|
504
|
-
|
|
505
|
-
const result = await params.runtimeContext.rewriteTranscriptEntries({
|
|
506
|
-
replacements: [{ entryId: targetEntry.id, message: modifiedMessage }],
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
ctx.log.info(`context-engine maintain: persisted memory to assistant entry ${targetEntry.id}`);
|
|
510
|
-
pendingInjection = null;
|
|
511
|
-
return result;
|
|
512
|
-
} catch (err) {
|
|
513
|
-
ctx.log.warn(`context-engine maintain failed: ${err}`);
|
|
514
|
-
pendingInjection = null;
|
|
515
|
-
return { ...noChange, reason: String(err) };
|
|
516
|
-
}
|
|
517
|
-
},
|
|
518
|
-
}));
|
|
519
|
-
|
|
520
|
-
ctx.log.info("memos-local: registered context engine 'memos-local-openclaw-plugin'");
|
|
521
|
-
} catch (err) {
|
|
522
|
-
ctx.log.warn(`memos-local: context engine registration failed (${err}), memory injection will use before_prompt_build fallback`);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ─── Memory Prompt Section: static instructions for the LLM ───
|
|
526
|
-
try {
|
|
527
|
-
api.registerMemoryPromptSection(() => [
|
|
528
|
-
"## Memory System",
|
|
529
|
-
"",
|
|
530
|
-
"Assistant messages in this conversation may contain <relevant-memories> blocks.",
|
|
531
|
-
"These are NOT part of the assistant's original response.",
|
|
532
|
-
"They contain background knowledge and memories relevant to the next user message,",
|
|
533
|
-
"injected by the user's local memory system before each query.",
|
|
534
|
-
"Use them as context to better understand and respond to the following user message.",
|
|
535
|
-
"Do not mention, quote, or repeat these memory blocks in your replies.",
|
|
536
|
-
"",
|
|
537
|
-
]);
|
|
538
|
-
ctx.log.info("memos-local: registered memory prompt section");
|
|
539
|
-
} catch (err) {
|
|
540
|
-
ctx.log.warn(`memos-local: registerMemoryPromptSection failed: ${err}`);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
404
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
544
405
|
async (...args: any[]) => {
|
|
545
406
|
const t0 = performance.now();
|
|
@@ -574,7 +435,6 @@ const memosLocalPlugin = {
|
|
|
574
435
|
}
|
|
575
436
|
};
|
|
576
437
|
|
|
577
|
-
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
578
438
|
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
579
439
|
scope === "group" || scope === "all" ? scope : "local";
|
|
580
440
|
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
@@ -608,6 +468,7 @@ const memosLocalPlugin = {
|
|
|
608
468
|
body: JSON.stringify({
|
|
609
469
|
memory: {
|
|
610
470
|
sourceChunkId: chunk.id,
|
|
471
|
+
sourceAgent: chunk.owner || "",
|
|
611
472
|
role: chunk.role,
|
|
612
473
|
content: chunk.content,
|
|
613
474
|
summary: chunk.summary,
|
|
@@ -628,6 +489,7 @@ const memosLocalPlugin = {
|
|
|
628
489
|
id: memoryId,
|
|
629
490
|
sourceChunkId: chunk.id,
|
|
630
491
|
sourceUserId: hubClient.userId,
|
|
492
|
+
sourceAgent: chunk.owner || "",
|
|
631
493
|
role: chunk.role,
|
|
632
494
|
content: chunk.content,
|
|
633
495
|
summary: chunk.summary ?? "",
|
|
@@ -665,7 +527,7 @@ const memosLocalPlugin = {
|
|
|
665
527
|
// ─── Tool: memory_search ───
|
|
666
528
|
|
|
667
529
|
api.registerTool(
|
|
668
|
-
{
|
|
530
|
+
(context) => ({
|
|
669
531
|
name: "memory_search",
|
|
670
532
|
label: "Memory Search",
|
|
671
533
|
description:
|
|
@@ -681,7 +543,7 @@ const memosLocalPlugin = {
|
|
|
681
543
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
682
544
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
683
545
|
}),
|
|
684
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any
|
|
546
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
685
547
|
const {
|
|
686
548
|
query,
|
|
687
549
|
scope: rawScope,
|
|
@@ -702,9 +564,6 @@ const memosLocalPlugin = {
|
|
|
702
564
|
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
703
565
|
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
704
566
|
let searchScope = resolveMemorySearchScope(rawScope);
|
|
705
|
-
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
706
|
-
searchScope = "all";
|
|
707
|
-
}
|
|
708
567
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
709
568
|
|
|
710
569
|
const agentId = context?.agentId ?? currentAgentId;
|
|
@@ -724,7 +583,7 @@ const memosLocalPlugin = {
|
|
|
724
583
|
|
|
725
584
|
// Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
|
|
726
585
|
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
727
|
-
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
586
|
+
const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
|
|
728
587
|
|
|
729
588
|
const rawLocalCandidates = localHits.map((h) => ({
|
|
730
589
|
chunkId: h.ref.chunkId,
|
|
@@ -733,6 +592,7 @@ const memosLocalPlugin = {
|
|
|
733
592
|
summary: h.summary,
|
|
734
593
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
735
594
|
origin: h.origin || "local",
|
|
595
|
+
owner: h.owner || "",
|
|
736
596
|
}));
|
|
737
597
|
|
|
738
598
|
// Hub remote candidates (from HTTP call) + hub-memory candidates (from RecallEngine for Hub role)
|
|
@@ -869,6 +729,7 @@ const memosLocalPlugin = {
|
|
|
869
729
|
chunkId: h.ref.chunkId, taskId: effectiveTaskId, skillId: h.skillId,
|
|
870
730
|
role: h.source.role, score: h.score, summary: h.summary,
|
|
871
731
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
732
|
+
owner: h.owner || "",
|
|
872
733
|
};
|
|
873
734
|
}),
|
|
874
735
|
...filteredHubRemoteHits.map((h: any) => ({
|
|
@@ -876,6 +737,7 @@ const memosLocalPlugin = {
|
|
|
876
737
|
role: h.source?.role ?? h.role ?? "assistant", score: h.score ?? 0,
|
|
877
738
|
summary: h.summary ?? "", original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
|
|
878
739
|
origin: "hub-remote", ownerName: h.ownerName ?? "", groupName: h.groupName ?? "",
|
|
740
|
+
sourceAgent: h.sourceAgent ?? "",
|
|
879
741
|
})),
|
|
880
742
|
];
|
|
881
743
|
|
|
@@ -889,14 +751,14 @@ const memosLocalPlugin = {
|
|
|
889
751
|
},
|
|
890
752
|
};
|
|
891
753
|
}),
|
|
892
|
-
},
|
|
754
|
+
}),
|
|
893
755
|
{ name: "memory_search" },
|
|
894
756
|
);
|
|
895
757
|
|
|
896
758
|
// ─── Tool: memory_timeline ───
|
|
897
759
|
|
|
898
760
|
api.registerTool(
|
|
899
|
-
{
|
|
761
|
+
(context) => ({
|
|
900
762
|
name: "memory_timeline",
|
|
901
763
|
label: "Memory Timeline",
|
|
902
764
|
description:
|
|
@@ -906,7 +768,7 @@ const memosLocalPlugin = {
|
|
|
906
768
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
907
769
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
908
770
|
}),
|
|
909
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any
|
|
771
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
910
772
|
const agentId = context?.agentId ?? currentAgentId;
|
|
911
773
|
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
912
774
|
const { chunkId, window: win } = params as {
|
|
@@ -950,14 +812,14 @@ const memosLocalPlugin = {
|
|
|
950
812
|
details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
|
|
951
813
|
};
|
|
952
814
|
}),
|
|
953
|
-
},
|
|
815
|
+
}),
|
|
954
816
|
{ name: "memory_timeline" },
|
|
955
817
|
);
|
|
956
818
|
|
|
957
819
|
// ─── Tool: memory_get ───
|
|
958
820
|
|
|
959
821
|
api.registerTool(
|
|
960
|
-
{
|
|
822
|
+
(context) => ({
|
|
961
823
|
name: "memory_get",
|
|
962
824
|
label: "Memory Get",
|
|
963
825
|
description:
|
|
@@ -968,7 +830,7 @@ const memosLocalPlugin = {
|
|
|
968
830
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
969
831
|
),
|
|
970
832
|
}),
|
|
971
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any
|
|
833
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
972
834
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
973
835
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
974
836
|
|
|
@@ -994,7 +856,7 @@ const memosLocalPlugin = {
|
|
|
994
856
|
},
|
|
995
857
|
};
|
|
996
858
|
}),
|
|
997
|
-
},
|
|
859
|
+
}),
|
|
998
860
|
{ name: "memory_get" },
|
|
999
861
|
);
|
|
1000
862
|
|
|
@@ -1335,6 +1197,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1335
1197
|
};
|
|
1336
1198
|
}
|
|
1337
1199
|
|
|
1200
|
+
const disabledWarning = skill.status === "archived"
|
|
1201
|
+
? "\n\n> **Warning:** This skill is currently **disabled** (archived). Its content is shown for reference only — it will not be used in search or auto-recall.\n\n"
|
|
1202
|
+
: "";
|
|
1203
|
+
|
|
1338
1204
|
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1339
1205
|
let footer = "\n\n---\n";
|
|
1340
1206
|
|
|
@@ -1359,7 +1225,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1359
1225
|
return {
|
|
1360
1226
|
content: [{
|
|
1361
1227
|
type: "text",
|
|
1362
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1228
|
+
text: `## Skill: ${skill.name} (v${skill.version})${disabledWarning}\n\n${sv.content}${footer}`,
|
|
1363
1229
|
}],
|
|
1364
1230
|
details: {
|
|
1365
1231
|
skillId: skill.id,
|
|
@@ -1516,7 +1382,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1516
1382
|
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
1517
1383
|
|
|
1518
1384
|
api.registerTool(
|
|
1519
|
-
{
|
|
1385
|
+
(context) => ({
|
|
1520
1386
|
name: "memory_viewer",
|
|
1521
1387
|
label: "Open Memory Viewer",
|
|
1522
1388
|
description:
|
|
@@ -1524,10 +1390,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1524
1390
|
"or access their stored memories, or asks where the memory dashboard is. " +
|
|
1525
1391
|
"Returns the URL the user can open in their browser.",
|
|
1526
1392
|
parameters: Type.Object({}),
|
|
1527
|
-
execute: trackTool("memory_viewer", async () => {
|
|
1393
|
+
execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
|
|
1528
1394
|
ctx.log.debug(`memory_viewer called`);
|
|
1529
1395
|
telemetry.trackViewerOpened();
|
|
1530
|
-
const
|
|
1396
|
+
const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
|
|
1397
|
+
const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
|
|
1531
1398
|
return {
|
|
1532
1399
|
content: [
|
|
1533
1400
|
{
|
|
@@ -1548,7 +1415,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1548
1415
|
details: { viewerUrl: url },
|
|
1549
1416
|
};
|
|
1550
1417
|
}),
|
|
1551
|
-
},
|
|
1418
|
+
}),
|
|
1552
1419
|
{ name: "memory_viewer" },
|
|
1553
1420
|
);
|
|
1554
1421
|
|
|
@@ -1779,7 +1646,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1779
1646
|
// ─── Tool: skill_search ───
|
|
1780
1647
|
|
|
1781
1648
|
api.registerTool(
|
|
1782
|
-
{
|
|
1649
|
+
(context) => ({
|
|
1783
1650
|
name: "skill_search",
|
|
1784
1651
|
label: "Skill Search",
|
|
1785
1652
|
description:
|
|
@@ -1789,10 +1656,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1789
1656
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1790
1657
|
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1791
1658
|
}),
|
|
1792
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any
|
|
1659
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1793
1660
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1794
1661
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1795
|
-
const
|
|
1662
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
1663
|
+
const currentOwner = `agent:${agentId}`;
|
|
1796
1664
|
|
|
1797
1665
|
if (rawScope === "group" || rawScope === "all") {
|
|
1798
1666
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1854,7 +1722,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1854
1722
|
details: { query: skillQuery, scope, hits },
|
|
1855
1723
|
};
|
|
1856
1724
|
}),
|
|
1857
|
-
},
|
|
1725
|
+
}),
|
|
1858
1726
|
{ name: "skill_search" },
|
|
1859
1727
|
);
|
|
1860
1728
|
|
|
@@ -1993,81 +1861,292 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1993
1861
|
{ name: "network_skill_pull" },
|
|
1994
1862
|
);
|
|
1995
1863
|
|
|
1996
|
-
// ───
|
|
1997
|
-
// Memory injection is handled by the Context Engine above.
|
|
1998
|
-
// This hook only handles skill auto-recall via prependContext.
|
|
1864
|
+
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1999
1865
|
|
|
2000
1866
|
api.on("before_prompt_build", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
2001
1867
|
if (!allowPromptInjection) return {};
|
|
2002
1868
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
2003
1869
|
|
|
2004
|
-
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
1870
|
+
const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
|
|
2005
1871
|
currentAgentId = recallAgentId;
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
if (!skillAutoRecall) return;
|
|
1872
|
+
const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
|
|
1873
|
+
ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
|
|
2009
1874
|
|
|
2010
1875
|
const recallT0 = performance.now();
|
|
1876
|
+
let recallQuery = "";
|
|
2011
1877
|
|
|
2012
1878
|
try {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
1879
|
+
const rawPrompt = event.prompt;
|
|
1880
|
+
ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
|
|
1881
|
+
|
|
1882
|
+
const query = normalizeAutoRecallQuery(rawPrompt);
|
|
1883
|
+
recallQuery = query;
|
|
1884
|
+
|
|
1885
|
+
if (query.length < 2) {
|
|
1886
|
+
ctx.log.debug("auto-recall: extracted query too short, skipping");
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
1890
|
+
|
|
1891
|
+
// ── Phase 1: Local search ∥ Hub search (parallel) ──
|
|
1892
|
+
const arLocalP = engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1893
|
+
const arHubP = ctx.config?.sharing?.enabled
|
|
1894
|
+
? hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" })
|
|
1895
|
+
.catch((err: any) => { ctx.log.debug(`auto-recall: hub search failed (${err})`); return { hits: [] as any[], meta: {} }; })
|
|
1896
|
+
: Promise.resolve({ hits: [] as any[], meta: {} });
|
|
1897
|
+
|
|
1898
|
+
const [result, arHubResult] = await Promise.all([arLocalP, arHubP]);
|
|
1899
|
+
|
|
1900
|
+
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
1901
|
+
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
1902
|
+
const hubRemoteHits: SearchHit[] = (arHubResult.hits ?? []).map((h: any) => ({
|
|
1903
|
+
summary: h.summary,
|
|
1904
|
+
original_excerpt: h.excerpt || h.summary,
|
|
1905
|
+
ref: { sessionKey: "", chunkId: h.remoteHitId ?? "", turnId: "", seq: 0 },
|
|
1906
|
+
score: 0.9,
|
|
1907
|
+
taskId: null,
|
|
1908
|
+
skillId: null,
|
|
1909
|
+
origin: "hub-remote" as const,
|
|
1910
|
+
source: { ts: h.source?.ts, role: h.source?.role ?? "assistant", sessionKey: "" },
|
|
1911
|
+
ownerName: h.ownerName,
|
|
1912
|
+
groupName: h.groupName,
|
|
1913
|
+
}));
|
|
1914
|
+
const allHubHits = [...hubLocalHits, ...hubRemoteHits];
|
|
1915
|
+
|
|
1916
|
+
ctx.log.debug(`auto-recall: local=${localHits.length}, hub-memory=${hubLocalHits.length}, hub-remote=${hubRemoteHits.length}`);
|
|
1917
|
+
|
|
1918
|
+
const rawLocalCandidates = localHits.map((h) => ({
|
|
1919
|
+
score: h.score, role: h.source.role, summary: h.summary,
|
|
1920
|
+
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
1921
|
+
owner: h.owner || "",
|
|
1922
|
+
}));
|
|
1923
|
+
const rawHubCandidates = allHubHits.map((h) => ({
|
|
1924
|
+
score: h.score, role: h.source.role, summary: h.summary,
|
|
1925
|
+
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "hub-remote",
|
|
1926
|
+
ownerName: (h as any).ownerName ?? "", groupName: (h as any).groupName ?? "",
|
|
1927
|
+
}));
|
|
1928
|
+
|
|
1929
|
+
const allRawHits = [...localHits, ...allHubHits];
|
|
1930
|
+
|
|
1931
|
+
if (allRawHits.length === 0) {
|
|
1932
|
+
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1933
|
+
const dur = performance.now() - recallT0;
|
|
1934
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1935
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1936
|
+
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1937
|
+
}), dur, true);
|
|
1938
|
+
|
|
1939
|
+
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1940
|
+
if (skillAutoRecallEarly) {
|
|
1941
|
+
try {
|
|
1942
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1943
|
+
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1944
|
+
const topSkills = skillHits.slice(0, skillLimit);
|
|
1945
|
+
if (topSkills.length > 0) {
|
|
1946
|
+
const skillLines = topSkills.map((sc, i) => {
|
|
1947
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1948
|
+
let badge = "";
|
|
1949
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1950
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1951
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1952
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1953
|
+
});
|
|
1954
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1955
|
+
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1956
|
+
skillLines.join("\n\n") +
|
|
1957
|
+
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1958
|
+
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1959
|
+
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1960
|
+
return { prependContext: skillContext };
|
|
1961
|
+
}
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (query.length > 50) {
|
|
1968
|
+
const noRecallHint =
|
|
1969
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1970
|
+
"Auto-recall found no results for a long query. " +
|
|
1971
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1972
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1973
|
+
return { prependContext: noRecallHint };
|
|
1974
|
+
}
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// ── Phase 2: Merge all → single LLM filter ──
|
|
1979
|
+
const mergedForFilter = allRawHits.map((h, i) => ({
|
|
1980
|
+
index: i + 1,
|
|
1981
|
+
role: h.source.role,
|
|
1982
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
1983
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
1984
|
+
}));
|
|
1985
|
+
|
|
1986
|
+
let filteredHits = allRawHits;
|
|
1987
|
+
let sufficient = false;
|
|
1988
|
+
|
|
1989
|
+
const filterResult = await summarizer.filterRelevant(query, mergedForFilter);
|
|
1990
|
+
if (filterResult !== null) {
|
|
1991
|
+
sufficient = filterResult.sufficient;
|
|
1992
|
+
if (filterResult.relevant.length > 0) {
|
|
1993
|
+
const indexSet = new Set(filterResult.relevant);
|
|
1994
|
+
filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1));
|
|
2022
1995
|
} else {
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
1996
|
+
const dur = performance.now() - recallT0;
|
|
1997
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1998
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1999
|
+
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
2000
|
+
}), dur, true);
|
|
2001
|
+
if (query.length > 50) {
|
|
2002
|
+
const noRecallHint =
|
|
2003
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
2004
|
+
"Auto-recall found no relevant results for a long query. " +
|
|
2005
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
2006
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
2007
|
+
return { prependContext: noRecallHint };
|
|
2026
2008
|
}
|
|
2009
|
+
return;
|
|
2027
2010
|
}
|
|
2028
2011
|
}
|
|
2029
|
-
query = stripInboundMetadata(query).replace(/<[^>]+>/g, "").trim();
|
|
2030
|
-
if (query.length < 2) return;
|
|
2031
2012
|
|
|
2013
|
+
const beforeDedup = filteredHits.length;
|
|
2014
|
+
filteredHits = deduplicateHits(filteredHits);
|
|
2015
|
+
ctx.log.debug(`auto-recall: merged ${allRawHits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
|
|
2016
|
+
|
|
2017
|
+
const lines = filteredHits.map((h, i) => {
|
|
2018
|
+
const excerpt = h.original_excerpt;
|
|
2019
|
+
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
2020
|
+
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
2021
|
+
if (excerpt) parts.push(` ${excerpt}`);
|
|
2022
|
+
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
2023
|
+
if (h.taskId) {
|
|
2024
|
+
const task = store.getTask(h.taskId);
|
|
2025
|
+
if (task && task.status !== "skipped") {
|
|
2026
|
+
parts.push(` task_id="${h.taskId}"`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return parts.join("\n");
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
const hasTask = filteredHits.some((h) => {
|
|
2033
|
+
if (!h.taskId) return false;
|
|
2034
|
+
const t = store.getTask(h.taskId);
|
|
2035
|
+
return t && t.status !== "skipped";
|
|
2036
|
+
});
|
|
2037
|
+
const tips: string[] = [];
|
|
2038
|
+
if (hasTask) {
|
|
2039
|
+
tips.push("- A hit has `task_id` → call `task_summary(taskId=\"...\")` to get the full task context (steps, code, results)");
|
|
2040
|
+
tips.push("- A task may have a reusable guide → call `skill_get(taskId=\"...\")` to retrieve the experience/skill");
|
|
2041
|
+
}
|
|
2042
|
+
tips.push("- Need more surrounding dialogue → call `memory_timeline(chunkId=\"...\")` to expand context around a hit");
|
|
2043
|
+
const tipsText = "\n\nAvailable follow-up tools:\n" + tips.join("\n");
|
|
2044
|
+
|
|
2045
|
+
const contextParts = [
|
|
2046
|
+
"## User's conversation history (from memory system)",
|
|
2047
|
+
"",
|
|
2048
|
+
"IMPORTANT: The following are facts from previous conversations with this user.",
|
|
2049
|
+
"You MUST treat these as established knowledge and use them directly when answering.",
|
|
2050
|
+
"Do NOT say you don't know or don't have information if the answer is in these memories.",
|
|
2051
|
+
"",
|
|
2052
|
+
lines.join("\n\n"),
|
|
2053
|
+
];
|
|
2054
|
+
if (tipsText) contextParts.push(tipsText);
|
|
2055
|
+
|
|
2056
|
+
// ─── Skill auto-recall ───
|
|
2057
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
2032
2058
|
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
2033
|
-
|
|
2059
|
+
let skillSection = "";
|
|
2034
2060
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2061
|
+
if (skillAutoRecall) {
|
|
2062
|
+
try {
|
|
2063
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
2064
|
+
|
|
2065
|
+
try {
|
|
2066
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
2067
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
2068
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
2069
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const taskIds = new Set<string>();
|
|
2077
|
+
for (const h of filteredHits) {
|
|
2078
|
+
if (h.taskId) {
|
|
2079
|
+
const t = store.getTask(h.taskId);
|
|
2080
|
+
if (t && t.status !== "skipped") taskIds.add(h.taskId);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
for (const tid of taskIds) {
|
|
2084
|
+
const linked = store.getSkillsByTask(tid);
|
|
2085
|
+
for (const rs of linked) {
|
|
2086
|
+
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
2087
|
+
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2040
2090
|
}
|
|
2091
|
+
|
|
2092
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
2093
|
+
|
|
2094
|
+
if (skillCandidates.length > 0) {
|
|
2095
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
2096
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
2097
|
+
let badge = "";
|
|
2098
|
+
if (manifest?.installed) badge = " [installed]";
|
|
2099
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
2100
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
2101
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
2102
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
2103
|
+
});
|
|
2104
|
+
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
2105
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
2106
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
2107
|
+
skillLines.join("\n\n");
|
|
2108
|
+
|
|
2109
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2110
|
+
try {
|
|
2111
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2112
|
+
} catch { /* best-effort */ }
|
|
2113
|
+
} else {
|
|
2114
|
+
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2115
|
+
}
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2041
2118
|
}
|
|
2042
|
-
} catch (err) {
|
|
2043
|
-
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2044
2119
|
}
|
|
2045
2120
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
} catch { /* best-effort */ }
|
|
2121
|
+
if (skillSection) contextParts.push(skillSection);
|
|
2122
|
+
const context = contextParts.join("\n");
|
|
2123
|
+
|
|
2124
|
+
const recallDur = performance.now() - recallT0;
|
|
2125
|
+
store.recordToolCall("memory_search", recallDur, true);
|
|
2126
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
2127
|
+
candidates: rawLocalCandidates,
|
|
2128
|
+
hubCandidates: rawHubCandidates,
|
|
2129
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local", owner: h.owner || "" })),
|
|
2130
|
+
}), recallDur, true);
|
|
2131
|
+
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
2132
|
+
|
|
2133
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
|
|
2134
|
+
|
|
2135
|
+
if (!sufficient) {
|
|
2136
|
+
const searchHint =
|
|
2137
|
+
"\n\nIf these memories don't fully answer the question, " +
|
|
2138
|
+
"call `memory_search` with a shorter or rephrased query to find more.";
|
|
2139
|
+
return { prependContext: context + searchHint };
|
|
2140
|
+
}
|
|
2067
2141
|
|
|
2068
|
-
return {
|
|
2142
|
+
return {
|
|
2143
|
+
prependContext: context,
|
|
2144
|
+
};
|
|
2069
2145
|
} catch (err) {
|
|
2070
|
-
|
|
2146
|
+
const dur = performance.now() - recallT0;
|
|
2147
|
+
store.recordToolCall("memory_search", dur, false);
|
|
2148
|
+
try { store.recordApiLog("memory_search", { type: "auto_recall", query: recallQuery }, `error: ${String(err)}`, dur, false); } catch (_) { /* best-effort */ }
|
|
2149
|
+
ctx.log.warn(`auto-recall failed: ${String(err)}`);
|
|
2071
2150
|
}
|
|
2072
2151
|
});
|
|
2073
2152
|
|
|
@@ -2083,7 +2162,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2083
2162
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
2084
2163
|
|
|
2085
2164
|
try {
|
|
2086
|
-
const captureAgentId = hookCtx?.agentId ?? "main";
|
|
2165
|
+
const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
|
|
2087
2166
|
currentAgentId = captureAgentId;
|
|
2088
2167
|
const captureOwner = `agent:${captureAgentId}`;
|
|
2089
2168
|
const sessionKey = hookCtx?.sessionKey ?? "default";
|
|
@@ -2301,48 +2380,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2301
2380
|
|
|
2302
2381
|
// ─── Service lifecycle ───
|
|
2303
2382
|
|
|
2304
|
-
|
|
2305
|
-
id: "memos-local-openclaw-plugin",
|
|
2306
|
-
start: async () => {
|
|
2307
|
-
if (hubServer) {
|
|
2308
|
-
const hubUrl = await hubServer.start();
|
|
2309
|
-
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
2310
|
-
}
|
|
2383
|
+
let serviceStarted = false;
|
|
2311
2384
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
}
|
|
2385
|
+
const startServiceCore = async () => {
|
|
2386
|
+
if (serviceStarted) return;
|
|
2387
|
+
serviceStarted = true;
|
|
2388
|
+
|
|
2389
|
+
if (hubServer) {
|
|
2390
|
+
const hubUrl = await hubServer.start();
|
|
2391
|
+
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
2392
|
+
}
|
|
2321
2393
|
|
|
2394
|
+
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
2322
2395
|
try {
|
|
2323
|
-
const
|
|
2324
|
-
api.logger.info(`memos-local:
|
|
2325
|
-
api.logger.info(`╔══════════════════════════════════════════╗`);
|
|
2326
|
-
api.logger.info(`║ MemOS Memory Viewer ║`);
|
|
2327
|
-
api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
|
|
2328
|
-
api.logger.info(`║ Open in browser to manage memories ║`);
|
|
2329
|
-
api.logger.info(`╚══════════════════════════════════════════╝`);
|
|
2330
|
-
api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
|
|
2331
|
-
api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
|
|
2332
|
-
skillEvolver.recoverOrphanedTasks().then((count) => {
|
|
2333
|
-
if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
|
|
2334
|
-
}).catch((err) => {
|
|
2335
|
-
api.logger.warn(`memos-local: skill recovery failed: ${err}`);
|
|
2336
|
-
});
|
|
2396
|
+
const session = await connectToHub(store, ctx.config, ctx.log);
|
|
2397
|
+
api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
|
|
2337
2398
|
} catch (err) {
|
|
2338
|
-
api.logger.warn(`memos-local:
|
|
2339
|
-
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2399
|
+
api.logger.warn(`memos-local: Hub connection failed: ${err}`);
|
|
2340
2400
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
);
|
|
2345
|
-
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
try {
|
|
2404
|
+
const viewerUrl = await viewer.start();
|
|
2405
|
+
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2406
|
+
api.logger.info(`╔══════════════════════════════════════════╗`);
|
|
2407
|
+
api.logger.info(`║ MemOS Memory Viewer ║`);
|
|
2408
|
+
api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
|
|
2409
|
+
api.logger.info(`║ Open in browser to manage memories ║`);
|
|
2410
|
+
api.logger.info(`╚══════════════════════════════════════════╝`);
|
|
2411
|
+
api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
|
|
2412
|
+
api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
|
|
2413
|
+
skillEvolver.recoverOrphanedTasks().then((count) => {
|
|
2414
|
+
if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
|
|
2415
|
+
}).catch((err) => {
|
|
2416
|
+
api.logger.warn(`memos-local: skill recovery failed: ${err}`);
|
|
2417
|
+
});
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
api.logger.warn(`memos-local: viewer failed to start: ${err}`);
|
|
2420
|
+
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2421
|
+
}
|
|
2422
|
+
telemetry.trackPluginStarted(
|
|
2423
|
+
ctx.config.embedding?.provider ?? "local",
|
|
2424
|
+
ctx.config.summarizer?.provider ?? "none",
|
|
2425
|
+
);
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
api.registerService({
|
|
2429
|
+
id: "memos-local-openclaw-plugin",
|
|
2430
|
+
start: async () => { await startServiceCore(); },
|
|
2346
2431
|
stop: async () => {
|
|
2347
2432
|
await worker.flush();
|
|
2348
2433
|
await telemetry.shutdown();
|
|
@@ -2352,6 +2437,21 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2352
2437
|
api.logger.info("memos-local: stopped");
|
|
2353
2438
|
},
|
|
2354
2439
|
});
|
|
2440
|
+
|
|
2441
|
+
// Fallback: OpenClaw may load this plugin via deferred reload after
|
|
2442
|
+
// startPluginServices has already run, so service.start() never fires.
|
|
2443
|
+
// Start on the next tick instead of waiting several seconds; the
|
|
2444
|
+
// serviceStarted guard still prevents duplicate startup if the host calls
|
|
2445
|
+
// service.start() immediately after registration.
|
|
2446
|
+
const SELF_START_DELAY_MS = 0;
|
|
2447
|
+
setTimeout(() => {
|
|
2448
|
+
if (!serviceStarted) {
|
|
2449
|
+
api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
|
|
2450
|
+
startServiceCore().catch((err) => {
|
|
2451
|
+
api.logger.warn(`memos-local: self-start failed: ${err}`);
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
}, SELF_START_DELAY_MS);
|
|
2355
2455
|
},
|
|
2356
2456
|
};
|
|
2357
2457
|
|