@memtensor/memos-local-openclaw-plugin 1.0.8-beta.7 → 1.0.8-beta.9
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 +334 -391
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -4
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/scripts/postinstall.cjs +25 -59
- package/src/context-engine/index.ts +321 -0
- package/src/hub/server.ts +6 -13
- package/src/ingest/providers/anthropic.ts +6 -9
- package/src/ingest/providers/bedrock.ts +6 -9
- package/src/ingest/providers/gemini.ts +6 -9
- package/src/ingest/providers/index.ts +22 -123
- package/src/ingest/providers/openai.ts +6 -141
- package/src/ingest/task-processor.ts +41 -61
- package/src/ingest/worker.ts +11 -32
- package/src/recall/engine.ts +1 -2
- package/src/sharing/types.ts +0 -1
- package/src/storage/sqlite.ts +11 -194
- package/src/types.ts +0 -3
- package/src/viewer/html.ts +266 -892
- package/src/viewer/server.ts +20 -293
- package/telemetry.credentials.json +5 -0
package/index.ts
CHANGED
|
@@ -31,6 +31,18 @@ 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";
|
|
34
46
|
|
|
35
47
|
|
|
36
48
|
/** Remove near-duplicate hits based on summary word overlap (>70%). Keeps first (highest-scored) hit. */
|
|
@@ -53,45 +65,6 @@ function deduplicateHits<T extends { summary: string }>(hits: T[]): T[] {
|
|
|
53
65
|
return kept;
|
|
54
66
|
}
|
|
55
67
|
|
|
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
|
-
function normalizeAutoRecallQuery(rawPrompt: string): string {
|
|
61
|
-
let query = rawPrompt.trim();
|
|
62
|
-
|
|
63
|
-
const senderTag = "Sender (untrusted metadata):";
|
|
64
|
-
const senderPos = query.indexOf(senderTag);
|
|
65
|
-
if (senderPos !== -1) {
|
|
66
|
-
const afterSender = query.slice(senderPos);
|
|
67
|
-
const fenceStart = afterSender.indexOf("```json");
|
|
68
|
-
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
69
|
-
if (fenceEnd > 0) {
|
|
70
|
-
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
71
|
-
} else {
|
|
72
|
-
const firstDblNl = afterSender.indexOf("\n\n");
|
|
73
|
-
if (firstDblNl > 0) {
|
|
74
|
-
query = afterSender.slice(firstDblNl + 2).trim();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
query = stripInboundMetadata(query);
|
|
80
|
-
query = query.replace(/<[^>]+>/g, "").trim();
|
|
81
|
-
|
|
82
|
-
if (NEW_SESSION_PROMPT_RE.test(query)) {
|
|
83
|
-
query = query.replace(NEW_SESSION_PROMPT_RE, "").trim();
|
|
84
|
-
query = query.replace(/^(Execute|Run) your Session Startup sequence[^\n]*\n?/im, "").trim();
|
|
85
|
-
query = query.replace(/^Current time:[^\n]*(\n|$)/im, "").trim();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
query = query.replace(INTERNAL_CONTEXT_RE, "").trim();
|
|
89
|
-
query = query.replace(CONTINUE_PROMPT_RE, "").trim();
|
|
90
|
-
|
|
91
|
-
return query;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
68
|
const pluginConfigSchema = {
|
|
96
69
|
type: "object" as const,
|
|
97
70
|
additionalProperties: true,
|
|
@@ -317,7 +290,7 @@ const memosLocalPlugin = {
|
|
|
317
290
|
const raw = fs.readFileSync(openclawJsonPath, "utf-8");
|
|
318
291
|
const cfg = JSON.parse(raw);
|
|
319
292
|
const allow: string[] | undefined = cfg?.tools?.allow;
|
|
320
|
-
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")
|
|
293
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")) {
|
|
321
294
|
const lastEntry = JSON.stringify(allow[allow.length - 1]);
|
|
322
295
|
const patched = raw.replace(
|
|
323
296
|
new RegExp(`(${lastEntry})(\\s*\\])`),
|
|
@@ -346,7 +319,6 @@ const memosLocalPlugin = {
|
|
|
346
319
|
// Current agent ID — updated by hooks, read by tools for owner isolation.
|
|
347
320
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
348
321
|
let currentAgentId = "main";
|
|
349
|
-
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
350
322
|
|
|
351
323
|
// ─── Check allowPromptInjection policy ───
|
|
352
324
|
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
@@ -360,6 +332,214 @@ const memosLocalPlugin = {
|
|
|
360
332
|
api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
|
|
361
333
|
}
|
|
362
334
|
|
|
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
|
+
|
|
363
543
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
364
544
|
async (...args: any[]) => {
|
|
365
545
|
const t0 = performance.now();
|
|
@@ -394,6 +574,7 @@ const memosLocalPlugin = {
|
|
|
394
574
|
}
|
|
395
575
|
};
|
|
396
576
|
|
|
577
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
397
578
|
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
398
579
|
scope === "group" || scope === "all" ? scope : "local";
|
|
399
580
|
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
@@ -427,7 +608,6 @@ const memosLocalPlugin = {
|
|
|
427
608
|
body: JSON.stringify({
|
|
428
609
|
memory: {
|
|
429
610
|
sourceChunkId: chunk.id,
|
|
430
|
-
sourceAgent: chunk.owner || "",
|
|
431
611
|
role: chunk.role,
|
|
432
612
|
content: chunk.content,
|
|
433
613
|
summary: chunk.summary,
|
|
@@ -448,7 +628,6 @@ const memosLocalPlugin = {
|
|
|
448
628
|
id: memoryId,
|
|
449
629
|
sourceChunkId: chunk.id,
|
|
450
630
|
sourceUserId: hubClient.userId,
|
|
451
|
-
sourceAgent: chunk.owner || "",
|
|
452
631
|
role: chunk.role,
|
|
453
632
|
content: chunk.content,
|
|
454
633
|
summary: chunk.summary ?? "",
|
|
@@ -486,7 +665,7 @@ const memosLocalPlugin = {
|
|
|
486
665
|
// ─── Tool: memory_search ───
|
|
487
666
|
|
|
488
667
|
api.registerTool(
|
|
489
|
-
|
|
668
|
+
{
|
|
490
669
|
name: "memory_search",
|
|
491
670
|
label: "Memory Search",
|
|
492
671
|
description:
|
|
@@ -502,7 +681,7 @@ const memosLocalPlugin = {
|
|
|
502
681
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
503
682
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
504
683
|
}),
|
|
505
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
684
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
506
685
|
const {
|
|
507
686
|
query,
|
|
508
687
|
scope: rawScope,
|
|
@@ -523,6 +702,9 @@ const memosLocalPlugin = {
|
|
|
523
702
|
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
524
703
|
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
525
704
|
let searchScope = resolveMemorySearchScope(rawScope);
|
|
705
|
+
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
706
|
+
searchScope = "all";
|
|
707
|
+
}
|
|
526
708
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
527
709
|
|
|
528
710
|
const agentId = context?.agentId ?? currentAgentId;
|
|
@@ -542,7 +724,7 @@ const memosLocalPlugin = {
|
|
|
542
724
|
|
|
543
725
|
// Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
|
|
544
726
|
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
545
|
-
const hubLocalHits =
|
|
727
|
+
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
546
728
|
|
|
547
729
|
const rawLocalCandidates = localHits.map((h) => ({
|
|
548
730
|
chunkId: h.ref.chunkId,
|
|
@@ -551,7 +733,6 @@ const memosLocalPlugin = {
|
|
|
551
733
|
summary: h.summary,
|
|
552
734
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
553
735
|
origin: h.origin || "local",
|
|
554
|
-
owner: h.owner || "",
|
|
555
736
|
}));
|
|
556
737
|
|
|
557
738
|
// Hub remote candidates (from HTTP call) + hub-memory candidates (from RecallEngine for Hub role)
|
|
@@ -688,7 +869,6 @@ const memosLocalPlugin = {
|
|
|
688
869
|
chunkId: h.ref.chunkId, taskId: effectiveTaskId, skillId: h.skillId,
|
|
689
870
|
role: h.source.role, score: h.score, summary: h.summary,
|
|
690
871
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
691
|
-
owner: h.owner || "",
|
|
692
872
|
};
|
|
693
873
|
}),
|
|
694
874
|
...filteredHubRemoteHits.map((h: any) => ({
|
|
@@ -696,7 +876,6 @@ const memosLocalPlugin = {
|
|
|
696
876
|
role: h.source?.role ?? h.role ?? "assistant", score: h.score ?? 0,
|
|
697
877
|
summary: h.summary ?? "", original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
|
|
698
878
|
origin: "hub-remote", ownerName: h.ownerName ?? "", groupName: h.groupName ?? "",
|
|
699
|
-
sourceAgent: h.sourceAgent ?? "",
|
|
700
879
|
})),
|
|
701
880
|
];
|
|
702
881
|
|
|
@@ -710,14 +889,14 @@ const memosLocalPlugin = {
|
|
|
710
889
|
},
|
|
711
890
|
};
|
|
712
891
|
}),
|
|
713
|
-
}
|
|
892
|
+
},
|
|
714
893
|
{ name: "memory_search" },
|
|
715
894
|
);
|
|
716
895
|
|
|
717
896
|
// ─── Tool: memory_timeline ───
|
|
718
897
|
|
|
719
898
|
api.registerTool(
|
|
720
|
-
|
|
899
|
+
{
|
|
721
900
|
name: "memory_timeline",
|
|
722
901
|
label: "Memory Timeline",
|
|
723
902
|
description:
|
|
@@ -727,7 +906,7 @@ const memosLocalPlugin = {
|
|
|
727
906
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
728
907
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
729
908
|
}),
|
|
730
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
909
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any, context?: any) => {
|
|
731
910
|
const agentId = context?.agentId ?? currentAgentId;
|
|
732
911
|
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
733
912
|
const { chunkId, window: win } = params as {
|
|
@@ -771,14 +950,14 @@ const memosLocalPlugin = {
|
|
|
771
950
|
details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
|
|
772
951
|
};
|
|
773
952
|
}),
|
|
774
|
-
}
|
|
953
|
+
},
|
|
775
954
|
{ name: "memory_timeline" },
|
|
776
955
|
);
|
|
777
956
|
|
|
778
957
|
// ─── Tool: memory_get ───
|
|
779
958
|
|
|
780
959
|
api.registerTool(
|
|
781
|
-
|
|
960
|
+
{
|
|
782
961
|
name: "memory_get",
|
|
783
962
|
label: "Memory Get",
|
|
784
963
|
description:
|
|
@@ -789,7 +968,7 @@ const memosLocalPlugin = {
|
|
|
789
968
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
790
969
|
),
|
|
791
970
|
}),
|
|
792
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
971
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
|
|
793
972
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
794
973
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
795
974
|
|
|
@@ -815,7 +994,7 @@ const memosLocalPlugin = {
|
|
|
815
994
|
},
|
|
816
995
|
};
|
|
817
996
|
}),
|
|
818
|
-
}
|
|
997
|
+
},
|
|
819
998
|
{ name: "memory_get" },
|
|
820
999
|
);
|
|
821
1000
|
|
|
@@ -1156,10 +1335,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1156
1335
|
};
|
|
1157
1336
|
}
|
|
1158
1337
|
|
|
1159
|
-
const disabledWarning = skill.status === "archived"
|
|
1160
|
-
? "\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"
|
|
1161
|
-
: "";
|
|
1162
|
-
|
|
1163
1338
|
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1164
1339
|
let footer = "\n\n---\n";
|
|
1165
1340
|
|
|
@@ -1184,7 +1359,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1184
1359
|
return {
|
|
1185
1360
|
content: [{
|
|
1186
1361
|
type: "text",
|
|
1187
|
-
text: `## Skill: ${skill.name} (v${skill.version})
|
|
1362
|
+
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1188
1363
|
}],
|
|
1189
1364
|
details: {
|
|
1190
1365
|
skillId: skill.id,
|
|
@@ -1341,7 +1516,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1341
1516
|
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
1342
1517
|
|
|
1343
1518
|
api.registerTool(
|
|
1344
|
-
|
|
1519
|
+
{
|
|
1345
1520
|
name: "memory_viewer",
|
|
1346
1521
|
label: "Open Memory Viewer",
|
|
1347
1522
|
description:
|
|
@@ -1349,11 +1524,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1349
1524
|
"or access their stored memories, or asks where the memory dashboard is. " +
|
|
1350
1525
|
"Returns the URL the user can open in their browser.",
|
|
1351
1526
|
parameters: Type.Object({}),
|
|
1352
|
-
execute: trackTool("memory_viewer", async (
|
|
1527
|
+
execute: trackTool("memory_viewer", async () => {
|
|
1353
1528
|
ctx.log.debug(`memory_viewer called`);
|
|
1354
1529
|
telemetry.trackViewerOpened();
|
|
1355
|
-
const
|
|
1356
|
-
const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
|
|
1530
|
+
const url = `http://127.0.0.1:${viewerPort}`;
|
|
1357
1531
|
return {
|
|
1358
1532
|
content: [
|
|
1359
1533
|
{
|
|
@@ -1374,7 +1548,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1374
1548
|
details: { viewerUrl: url },
|
|
1375
1549
|
};
|
|
1376
1550
|
}),
|
|
1377
|
-
}
|
|
1551
|
+
},
|
|
1378
1552
|
{ name: "memory_viewer" },
|
|
1379
1553
|
);
|
|
1380
1554
|
|
|
@@ -1605,7 +1779,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1605
1779
|
// ─── Tool: skill_search ───
|
|
1606
1780
|
|
|
1607
1781
|
api.registerTool(
|
|
1608
|
-
|
|
1782
|
+
{
|
|
1609
1783
|
name: "skill_search",
|
|
1610
1784
|
label: "Skill Search",
|
|
1611
1785
|
description:
|
|
@@ -1615,11 +1789,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1615
1789
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1616
1790
|
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1617
1791
|
}),
|
|
1618
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1792
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
1619
1793
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1620
1794
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1621
|
-
const
|
|
1622
|
-
const currentOwner = `agent:${agentId}`;
|
|
1795
|
+
const currentOwner = getCurrentOwner();
|
|
1623
1796
|
|
|
1624
1797
|
if (rawScope === "group" || rawScope === "all") {
|
|
1625
1798
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1681,7 +1854,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1681
1854
|
details: { query: skillQuery, scope, hits },
|
|
1682
1855
|
};
|
|
1683
1856
|
}),
|
|
1684
|
-
}
|
|
1857
|
+
},
|
|
1685
1858
|
{ name: "skill_search" },
|
|
1686
1859
|
);
|
|
1687
1860
|
|
|
@@ -1820,292 +1993,81 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1820
1993
|
{ name: "network_skill_pull" },
|
|
1821
1994
|
);
|
|
1822
1995
|
|
|
1823
|
-
// ───
|
|
1996
|
+
// ─── Skill auto-recall: inject relevant skills before agent starts ───
|
|
1997
|
+
// Memory injection is handled by the Context Engine above.
|
|
1998
|
+
// This hook only handles skill auto-recall via prependContext.
|
|
1824
1999
|
|
|
1825
2000
|
api.on("before_prompt_build", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
1826
2001
|
if (!allowPromptInjection) return {};
|
|
1827
2002
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1828
2003
|
|
|
1829
|
-
const recallAgentId = hookCtx?.agentId ??
|
|
2004
|
+
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
1830
2005
|
currentAgentId = recallAgentId;
|
|
1831
|
-
|
|
1832
|
-
ctx.
|
|
2006
|
+
|
|
2007
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
2008
|
+
if (!skillAutoRecall) return;
|
|
1833
2009
|
|
|
1834
2010
|
const recallT0 = performance.now();
|
|
1835
|
-
let recallQuery = "";
|
|
1836
2011
|
|
|
1837
2012
|
try {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
1849
|
-
|
|
1850
|
-
// ── Phase 1: Local search ∥ Hub search (parallel) ──
|
|
1851
|
-
const arLocalP = engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1852
|
-
const arHubP = ctx.config?.sharing?.enabled
|
|
1853
|
-
? hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" })
|
|
1854
|
-
.catch((err: any) => { ctx.log.debug(`auto-recall: hub search failed (${err})`); return { hits: [] as any[], meta: {} }; })
|
|
1855
|
-
: Promise.resolve({ hits: [] as any[], meta: {} });
|
|
1856
|
-
|
|
1857
|
-
const [result, arHubResult] = await Promise.all([arLocalP, arHubP]);
|
|
1858
|
-
|
|
1859
|
-
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
1860
|
-
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
1861
|
-
const hubRemoteHits: SearchHit[] = (arHubResult.hits ?? []).map((h: any) => ({
|
|
1862
|
-
summary: h.summary,
|
|
1863
|
-
original_excerpt: h.excerpt || h.summary,
|
|
1864
|
-
ref: { sessionKey: "", chunkId: h.remoteHitId ?? "", turnId: "", seq: 0 },
|
|
1865
|
-
score: 0.9,
|
|
1866
|
-
taskId: null,
|
|
1867
|
-
skillId: null,
|
|
1868
|
-
origin: "hub-remote" as const,
|
|
1869
|
-
source: { ts: h.source?.ts, role: h.source?.role ?? "assistant", sessionKey: "" },
|
|
1870
|
-
ownerName: h.ownerName,
|
|
1871
|
-
groupName: h.groupName,
|
|
1872
|
-
}));
|
|
1873
|
-
const allHubHits = [...hubLocalHits, ...hubRemoteHits];
|
|
1874
|
-
|
|
1875
|
-
ctx.log.debug(`auto-recall: local=${localHits.length}, hub-memory=${hubLocalHits.length}, hub-remote=${hubRemoteHits.length}`);
|
|
1876
|
-
|
|
1877
|
-
const rawLocalCandidates = localHits.map((h) => ({
|
|
1878
|
-
score: h.score, role: h.source.role, summary: h.summary,
|
|
1879
|
-
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
1880
|
-
owner: h.owner || "",
|
|
1881
|
-
}));
|
|
1882
|
-
const rawHubCandidates = allHubHits.map((h) => ({
|
|
1883
|
-
score: h.score, role: h.source.role, summary: h.summary,
|
|
1884
|
-
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "hub-remote",
|
|
1885
|
-
ownerName: (h as any).ownerName ?? "", groupName: (h as any).groupName ?? "",
|
|
1886
|
-
}));
|
|
1887
|
-
|
|
1888
|
-
const allRawHits = [...localHits, ...allHubHits];
|
|
1889
|
-
|
|
1890
|
-
if (allRawHits.length === 0) {
|
|
1891
|
-
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1892
|
-
const dur = performance.now() - recallT0;
|
|
1893
|
-
store.recordToolCall("memory_search", dur, true);
|
|
1894
|
-
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1895
|
-
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1896
|
-
}), dur, true);
|
|
1897
|
-
|
|
1898
|
-
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1899
|
-
if (skillAutoRecallEarly) {
|
|
1900
|
-
try {
|
|
1901
|
-
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1902
|
-
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1903
|
-
const topSkills = skillHits.slice(0, skillLimit);
|
|
1904
|
-
if (topSkills.length > 0) {
|
|
1905
|
-
const skillLines = topSkills.map((sc, i) => {
|
|
1906
|
-
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1907
|
-
let badge = "";
|
|
1908
|
-
if (manifest?.installed) badge = " [installed]";
|
|
1909
|
-
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1910
|
-
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1911
|
-
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1912
|
-
});
|
|
1913
|
-
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1914
|
-
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1915
|
-
skillLines.join("\n\n") +
|
|
1916
|
-
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1917
|
-
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1918
|
-
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1919
|
-
return { prependContext: skillContext };
|
|
1920
|
-
}
|
|
1921
|
-
} catch (err) {
|
|
1922
|
-
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
if (query.length > 50) {
|
|
1927
|
-
const noRecallHint =
|
|
1928
|
-
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1929
|
-
"Auto-recall found no results for a long query. " +
|
|
1930
|
-
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1931
|
-
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1932
|
-
return { prependContext: noRecallHint };
|
|
1933
|
-
}
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
// ── Phase 2: Merge all → single LLM filter ──
|
|
1938
|
-
const mergedForFilter = allRawHits.map((h, i) => ({
|
|
1939
|
-
index: i + 1,
|
|
1940
|
-
role: h.source.role,
|
|
1941
|
-
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
1942
|
-
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
1943
|
-
}));
|
|
1944
|
-
|
|
1945
|
-
let filteredHits = allRawHits;
|
|
1946
|
-
let sufficient = false;
|
|
1947
|
-
|
|
1948
|
-
const filterResult = await summarizer.filterRelevant(query, mergedForFilter);
|
|
1949
|
-
if (filterResult !== null) {
|
|
1950
|
-
sufficient = filterResult.sufficient;
|
|
1951
|
-
if (filterResult.relevant.length > 0) {
|
|
1952
|
-
const indexSet = new Set(filterResult.relevant);
|
|
1953
|
-
filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1));
|
|
2013
|
+
let query = event.prompt;
|
|
2014
|
+
const senderTag = "Sender (untrusted metadata):";
|
|
2015
|
+
const senderPos = query.indexOf(senderTag);
|
|
2016
|
+
if (senderPos !== -1) {
|
|
2017
|
+
const afterSender = query.slice(senderPos);
|
|
2018
|
+
const fenceStart = afterSender.indexOf("```json");
|
|
2019
|
+
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
2020
|
+
if (fenceEnd > 0) {
|
|
2021
|
+
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
1954
2022
|
} else {
|
|
1955
|
-
const
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1959
|
-
}), dur, true);
|
|
1960
|
-
if (query.length > 50) {
|
|
1961
|
-
const noRecallHint =
|
|
1962
|
-
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1963
|
-
"Auto-recall found no relevant results for a long query. " +
|
|
1964
|
-
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1965
|
-
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1966
|
-
return { prependContext: noRecallHint };
|
|
2023
|
+
const firstDblNl = afterSender.indexOf("\n\n");
|
|
2024
|
+
if (firstDblNl > 0) {
|
|
2025
|
+
query = afterSender.slice(firstDblNl + 2).trim();
|
|
1967
2026
|
}
|
|
1968
|
-
return;
|
|
1969
2027
|
}
|
|
1970
2028
|
}
|
|
2029
|
+
query = stripInboundMetadata(query).replace(/<[^>]+>/g, "").trim();
|
|
2030
|
+
if (query.length < 2) return;
|
|
1971
2031
|
|
|
1972
|
-
const beforeDedup = filteredHits.length;
|
|
1973
|
-
filteredHits = deduplicateHits(filteredHits);
|
|
1974
|
-
ctx.log.debug(`auto-recall: merged ${allRawHits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
|
|
1975
|
-
|
|
1976
|
-
const lines = filteredHits.map((h, i) => {
|
|
1977
|
-
const excerpt = h.original_excerpt;
|
|
1978
|
-
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
1979
|
-
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
1980
|
-
if (excerpt) parts.push(` ${excerpt}`);
|
|
1981
|
-
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
1982
|
-
if (h.taskId) {
|
|
1983
|
-
const task = store.getTask(h.taskId);
|
|
1984
|
-
if (task && task.status !== "skipped") {
|
|
1985
|
-
parts.push(` task_id="${h.taskId}"`);
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
return parts.join("\n");
|
|
1989
|
-
});
|
|
1990
|
-
|
|
1991
|
-
const hasTask = filteredHits.some((h) => {
|
|
1992
|
-
if (!h.taskId) return false;
|
|
1993
|
-
const t = store.getTask(h.taskId);
|
|
1994
|
-
return t && t.status !== "skipped";
|
|
1995
|
-
});
|
|
1996
|
-
const tips: string[] = [];
|
|
1997
|
-
if (hasTask) {
|
|
1998
|
-
tips.push("- A hit has `task_id` → call `task_summary(taskId=\"...\")` to get the full task context (steps, code, results)");
|
|
1999
|
-
tips.push("- A task may have a reusable guide → call `skill_get(taskId=\"...\")` to retrieve the experience/skill");
|
|
2000
|
-
}
|
|
2001
|
-
tips.push("- Need more surrounding dialogue → call `memory_timeline(chunkId=\"...\")` to expand context around a hit");
|
|
2002
|
-
const tipsText = "\n\nAvailable follow-up tools:\n" + tips.join("\n");
|
|
2003
|
-
|
|
2004
|
-
const contextParts = [
|
|
2005
|
-
"## User's conversation history (from memory system)",
|
|
2006
|
-
"",
|
|
2007
|
-
"IMPORTANT: The following are facts from previous conversations with this user.",
|
|
2008
|
-
"You MUST treat these as established knowledge and use them directly when answering.",
|
|
2009
|
-
"Do NOT say you don't know or don't have information if the answer is in these memories.",
|
|
2010
|
-
"",
|
|
2011
|
-
lines.join("\n\n"),
|
|
2012
|
-
];
|
|
2013
|
-
if (tipsText) contextParts.push(tipsText);
|
|
2014
|
-
|
|
2015
|
-
// ─── Skill auto-recall ───
|
|
2016
|
-
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
2017
2032
|
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
if (skillAutoRecall) {
|
|
2021
|
-
try {
|
|
2022
|
-
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
2023
|
-
|
|
2024
|
-
try {
|
|
2025
|
-
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
2026
|
-
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
2027
|
-
if (!skillCandidateMap.has(sh.skillId)) {
|
|
2028
|
-
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
} catch (err) {
|
|
2032
|
-
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2033
|
-
}
|
|
2033
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
2034
2034
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
for (const tid of taskIds) {
|
|
2043
|
-
const linked = store.getSkillsByTask(tid);
|
|
2044
|
-
for (const rs of linked) {
|
|
2045
|
-
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
2046
|
-
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
2052
|
-
|
|
2053
|
-
if (skillCandidates.length > 0) {
|
|
2054
|
-
const skillLines = skillCandidates.map((sc, i) => {
|
|
2055
|
-
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
2056
|
-
let badge = "";
|
|
2057
|
-
if (manifest?.installed) badge = " [installed]";
|
|
2058
|
-
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
2059
|
-
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
2060
|
-
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
2061
|
-
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
2062
|
-
});
|
|
2063
|
-
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
2064
|
-
"The following skills were distilled from similar previous tasks. " +
|
|
2065
|
-
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
2066
|
-
skillLines.join("\n\n");
|
|
2067
|
-
|
|
2068
|
-
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2069
|
-
try {
|
|
2070
|
-
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2071
|
-
} catch { /* best-effort */ }
|
|
2072
|
-
} else {
|
|
2073
|
-
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2035
|
+
try {
|
|
2036
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
2037
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
2038
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
2039
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
2074
2040
|
}
|
|
2075
|
-
} catch (err) {
|
|
2076
|
-
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2077
2041
|
}
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2078
2044
|
}
|
|
2079
2045
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2046
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
2047
|
+
if (skillCandidates.length === 0) return;
|
|
2048
|
+
|
|
2049
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
2050
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
2051
|
+
let badge = "";
|
|
2052
|
+
if (manifest?.installed) badge = " [installed]";
|
|
2053
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
2054
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
2055
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
2056
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
2057
|
+
});
|
|
2058
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
2059
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
2060
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
2061
|
+
skillLines.join("\n\n");
|
|
2062
|
+
|
|
2063
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2064
|
+
try {
|
|
2065
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2066
|
+
} catch { /* best-effort */ }
|
|
2100
2067
|
|
|
2101
|
-
return {
|
|
2102
|
-
prependContext: context,
|
|
2103
|
-
};
|
|
2068
|
+
return { prependContext: skillContext };
|
|
2104
2069
|
} catch (err) {
|
|
2105
|
-
|
|
2106
|
-
store.recordToolCall("memory_search", dur, false);
|
|
2107
|
-
try { store.recordApiLog("memory_search", { type: "auto_recall", query: recallQuery }, `error: ${String(err)}`, dur, false); } catch (_) { /* best-effort */ }
|
|
2108
|
-
ctx.log.warn(`auto-recall failed: ${String(err)}`);
|
|
2070
|
+
ctx.log.warn(`auto-recall-skill failed: ${String(err)}`);
|
|
2109
2071
|
}
|
|
2110
2072
|
});
|
|
2111
2073
|
|
|
@@ -2121,7 +2083,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2121
2083
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
2122
2084
|
|
|
2123
2085
|
try {
|
|
2124
|
-
const captureAgentId = hookCtx?.agentId ??
|
|
2086
|
+
const captureAgentId = hookCtx?.agentId ?? "main";
|
|
2125
2087
|
currentAgentId = captureAgentId;
|
|
2126
2088
|
const captureOwner = `agent:${captureAgentId}`;
|
|
2127
2089
|
const sessionKey = hookCtx?.sessionKey ?? "default";
|
|
@@ -2339,54 +2301,48 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2339
2301
|
|
|
2340
2302
|
// ─── Service lifecycle ───
|
|
2341
2303
|
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2304
|
+
api.registerService({
|
|
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
|
+
}
|
|
2347
2311
|
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2312
|
+
// Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
|
|
2313
|
+
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
2314
|
+
try {
|
|
2315
|
+
const session = await connectToHub(store, ctx.config, ctx.log);
|
|
2316
|
+
api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
|
|
2317
|
+
} catch (err) {
|
|
2318
|
+
api.logger.warn(`memos-local: Hub connection failed: ${err}`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2352
2321
|
|
|
2353
|
-
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
2354
2322
|
try {
|
|
2355
|
-
const
|
|
2356
|
-
api.logger.info(`memos-local:
|
|
2323
|
+
const viewerUrl = await viewer.start();
|
|
2324
|
+
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
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
|
+
});
|
|
2357
2337
|
} catch (err) {
|
|
2358
|
-
api.logger.warn(`memos-local:
|
|
2338
|
+
api.logger.warn(`memos-local: viewer failed to start: ${err}`);
|
|
2339
|
+
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2359
2340
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
api.logger.info(`╔══════════════════════════════════════════╗`);
|
|
2366
|
-
api.logger.info(`║ MemOS Memory Viewer ║`);
|
|
2367
|
-
api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
|
|
2368
|
-
api.logger.info(`║ Open in browser to manage memories ║`);
|
|
2369
|
-
api.logger.info(`╚══════════════════════════════════════════╝`);
|
|
2370
|
-
api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
|
|
2371
|
-
api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
|
|
2372
|
-
skillEvolver.recoverOrphanedTasks().then((count) => {
|
|
2373
|
-
if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
|
|
2374
|
-
}).catch((err) => {
|
|
2375
|
-
api.logger.warn(`memos-local: skill recovery failed: ${err}`);
|
|
2376
|
-
});
|
|
2377
|
-
} catch (err) {
|
|
2378
|
-
api.logger.warn(`memos-local: viewer failed to start: ${err}`);
|
|
2379
|
-
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2380
|
-
}
|
|
2381
|
-
telemetry.trackPluginStarted(
|
|
2382
|
-
ctx.config.embedding?.provider ?? "local",
|
|
2383
|
-
ctx.config.summarizer?.provider ?? "none",
|
|
2384
|
-
);
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
api.registerService({
|
|
2388
|
-
id: "memos-local-openclaw-plugin",
|
|
2389
|
-
start: async () => { await startServiceCore(); },
|
|
2341
|
+
telemetry.trackPluginStarted(
|
|
2342
|
+
ctx.config.embedding?.provider ?? "local",
|
|
2343
|
+
ctx.config.summarizer?.provider ?? "none",
|
|
2344
|
+
);
|
|
2345
|
+
},
|
|
2390
2346
|
stop: async () => {
|
|
2391
2347
|
await worker.flush();
|
|
2392
2348
|
await telemetry.shutdown();
|
|
@@ -2396,19 +2352,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2396
2352
|
api.logger.info("memos-local: stopped");
|
|
2397
2353
|
},
|
|
2398
2354
|
});
|
|
2399
|
-
|
|
2400
|
-
// Fallback: OpenClaw may load this plugin via deferred reload after
|
|
2401
|
-
// startPluginServices has already run, so service.start() never fires.
|
|
2402
|
-
// Self-start the viewer after a grace period if it hasn't been started.
|
|
2403
|
-
const SELF_START_DELAY_MS = 3000;
|
|
2404
|
-
setTimeout(() => {
|
|
2405
|
-
if (!serviceStarted) {
|
|
2406
|
-
api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
|
|
2407
|
-
startServiceCore().catch((err) => {
|
|
2408
|
-
api.logger.warn(`memos-local: self-start failed: ${err}`);
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
}, SELF_START_DELAY_MS);
|
|
2412
2355
|
},
|
|
2413
2356
|
};
|
|
2414
2357
|
|