@memtensor/memos-local-openclaw-plugin 1.0.7-beta.1 → 1.0.7-beta.3
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/.env.example +4 -0
- package/index.ts +291 -285
- package/package.json +1 -1
- package/scripts/postinstall.cjs +18 -9
- package/src/recall/engine.ts +1 -1
- package/src/storage/sqlite.ts +10 -2
- package/src/viewer/html.ts +31 -27
- package/src/viewer/server.ts +1 -1
- 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/src/context-engine/index.ts +0 -321
- package/telemetry.credentials.json +0 -5
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. */
|
|
@@ -319,6 +307,7 @@ const memosLocalPlugin = {
|
|
|
319
307
|
// Current agent ID — updated by hooks, read by tools for owner isolation.
|
|
320
308
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
321
309
|
let currentAgentId = "main";
|
|
310
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
322
311
|
|
|
323
312
|
// ─── Check allowPromptInjection policy ───
|
|
324
313
|
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
@@ -332,214 +321,6 @@ const memosLocalPlugin = {
|
|
|
332
321
|
api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
|
|
333
322
|
}
|
|
334
323
|
|
|
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
324
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
544
325
|
async (...args: any[]) => {
|
|
545
326
|
const t0 = performance.now();
|
|
@@ -574,7 +355,6 @@ const memosLocalPlugin = {
|
|
|
574
355
|
}
|
|
575
356
|
};
|
|
576
357
|
|
|
577
|
-
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
578
358
|
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
579
359
|
scope === "group" || scope === "all" ? scope : "local";
|
|
580
360
|
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
@@ -665,7 +445,7 @@ const memosLocalPlugin = {
|
|
|
665
445
|
// ─── Tool: memory_search ───
|
|
666
446
|
|
|
667
447
|
api.registerTool(
|
|
668
|
-
{
|
|
448
|
+
(context) => ({
|
|
669
449
|
name: "memory_search",
|
|
670
450
|
label: "Memory Search",
|
|
671
451
|
description:
|
|
@@ -681,7 +461,7 @@ const memosLocalPlugin = {
|
|
|
681
461
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
682
462
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
683
463
|
}),
|
|
684
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any
|
|
464
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
685
465
|
const {
|
|
686
466
|
query,
|
|
687
467
|
scope: rawScope,
|
|
@@ -702,9 +482,6 @@ const memosLocalPlugin = {
|
|
|
702
482
|
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
703
483
|
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
704
484
|
let searchScope = resolveMemorySearchScope(rawScope);
|
|
705
|
-
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
706
|
-
searchScope = "all";
|
|
707
|
-
}
|
|
708
485
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
709
486
|
|
|
710
487
|
const agentId = context?.agentId ?? currentAgentId;
|
|
@@ -724,7 +501,7 @@ const memosLocalPlugin = {
|
|
|
724
501
|
|
|
725
502
|
// Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
|
|
726
503
|
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
727
|
-
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
504
|
+
const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
|
|
728
505
|
|
|
729
506
|
const rawLocalCandidates = localHits.map((h) => ({
|
|
730
507
|
chunkId: h.ref.chunkId,
|
|
@@ -889,14 +666,14 @@ const memosLocalPlugin = {
|
|
|
889
666
|
},
|
|
890
667
|
};
|
|
891
668
|
}),
|
|
892
|
-
},
|
|
669
|
+
}),
|
|
893
670
|
{ name: "memory_search" },
|
|
894
671
|
);
|
|
895
672
|
|
|
896
673
|
// ─── Tool: memory_timeline ───
|
|
897
674
|
|
|
898
675
|
api.registerTool(
|
|
899
|
-
{
|
|
676
|
+
(context) => ({
|
|
900
677
|
name: "memory_timeline",
|
|
901
678
|
label: "Memory Timeline",
|
|
902
679
|
description:
|
|
@@ -906,7 +683,7 @@ const memosLocalPlugin = {
|
|
|
906
683
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
907
684
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
908
685
|
}),
|
|
909
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any
|
|
686
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
910
687
|
const agentId = context?.agentId ?? currentAgentId;
|
|
911
688
|
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
912
689
|
const { chunkId, window: win } = params as {
|
|
@@ -950,14 +727,14 @@ const memosLocalPlugin = {
|
|
|
950
727
|
details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
|
|
951
728
|
};
|
|
952
729
|
}),
|
|
953
|
-
},
|
|
730
|
+
}),
|
|
954
731
|
{ name: "memory_timeline" },
|
|
955
732
|
);
|
|
956
733
|
|
|
957
734
|
// ─── Tool: memory_get ───
|
|
958
735
|
|
|
959
736
|
api.registerTool(
|
|
960
|
-
{
|
|
737
|
+
(context) => ({
|
|
961
738
|
name: "memory_get",
|
|
962
739
|
label: "Memory Get",
|
|
963
740
|
description:
|
|
@@ -968,7 +745,7 @@ const memosLocalPlugin = {
|
|
|
968
745
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
969
746
|
),
|
|
970
747
|
}),
|
|
971
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any
|
|
748
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
972
749
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
973
750
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
974
751
|
|
|
@@ -994,7 +771,7 @@ const memosLocalPlugin = {
|
|
|
994
771
|
},
|
|
995
772
|
};
|
|
996
773
|
}),
|
|
997
|
-
},
|
|
774
|
+
}),
|
|
998
775
|
{ name: "memory_get" },
|
|
999
776
|
);
|
|
1000
777
|
|
|
@@ -1516,7 +1293,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1516
1293
|
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
1517
1294
|
|
|
1518
1295
|
api.registerTool(
|
|
1519
|
-
{
|
|
1296
|
+
(context) => ({
|
|
1520
1297
|
name: "memory_viewer",
|
|
1521
1298
|
label: "Open Memory Viewer",
|
|
1522
1299
|
description:
|
|
@@ -1524,10 +1301,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1524
1301
|
"or access their stored memories, or asks where the memory dashboard is. " +
|
|
1525
1302
|
"Returns the URL the user can open in their browser.",
|
|
1526
1303
|
parameters: Type.Object({}),
|
|
1527
|
-
execute: trackTool("memory_viewer", async () => {
|
|
1304
|
+
execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
|
|
1528
1305
|
ctx.log.debug(`memory_viewer called`);
|
|
1529
1306
|
telemetry.trackViewerOpened();
|
|
1530
|
-
const
|
|
1307
|
+
const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
|
|
1308
|
+
const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
|
|
1531
1309
|
return {
|
|
1532
1310
|
content: [
|
|
1533
1311
|
{
|
|
@@ -1548,7 +1326,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1548
1326
|
details: { viewerUrl: url },
|
|
1549
1327
|
};
|
|
1550
1328
|
}),
|
|
1551
|
-
},
|
|
1329
|
+
}),
|
|
1552
1330
|
{ name: "memory_viewer" },
|
|
1553
1331
|
);
|
|
1554
1332
|
|
|
@@ -1779,7 +1557,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1779
1557
|
// ─── Tool: skill_search ───
|
|
1780
1558
|
|
|
1781
1559
|
api.registerTool(
|
|
1782
|
-
{
|
|
1560
|
+
(context) => ({
|
|
1783
1561
|
name: "skill_search",
|
|
1784
1562
|
label: "Skill Search",
|
|
1785
1563
|
description:
|
|
@@ -1789,10 +1567,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1789
1567
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1790
1568
|
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1791
1569
|
}),
|
|
1792
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any
|
|
1570
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1793
1571
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1794
1572
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1795
|
-
const
|
|
1573
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
1574
|
+
const currentOwner = `agent:${agentId}`;
|
|
1796
1575
|
|
|
1797
1576
|
if (rawScope === "group" || rawScope === "all") {
|
|
1798
1577
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1854,7 +1633,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1854
1633
|
details: { query: skillQuery, scope, hits },
|
|
1855
1634
|
};
|
|
1856
1635
|
}),
|
|
1857
|
-
},
|
|
1636
|
+
}),
|
|
1858
1637
|
{ name: "skill_search" },
|
|
1859
1638
|
);
|
|
1860
1639
|
|
|
@@ -1993,28 +1772,29 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1993
1772
|
{ name: "network_skill_pull" },
|
|
1994
1773
|
);
|
|
1995
1774
|
|
|
1996
|
-
// ───
|
|
1997
|
-
// Memory injection is handled by the Context Engine above.
|
|
1998
|
-
// This hook only handles skill auto-recall via prependContext.
|
|
1775
|
+
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1999
1776
|
|
|
2000
1777
|
api.on("before_prompt_build", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
2001
1778
|
if (!allowPromptInjection) return {};
|
|
2002
1779
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
2003
1780
|
|
|
2004
|
-
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
1781
|
+
const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
|
|
2005
1782
|
currentAgentId = recallAgentId;
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
if (!skillAutoRecall) return;
|
|
1783
|
+
const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
|
|
1784
|
+
ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
|
|
2009
1785
|
|
|
2010
1786
|
const recallT0 = performance.now();
|
|
1787
|
+
let recallQuery = "";
|
|
2011
1788
|
|
|
2012
1789
|
try {
|
|
2013
|
-
|
|
1790
|
+
const rawPrompt = event.prompt;
|
|
1791
|
+
ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
|
|
1792
|
+
|
|
1793
|
+
let query = rawPrompt;
|
|
2014
1794
|
const senderTag = "Sender (untrusted metadata):";
|
|
2015
|
-
const senderPos =
|
|
1795
|
+
const senderPos = rawPrompt.indexOf(senderTag);
|
|
2016
1796
|
if (senderPos !== -1) {
|
|
2017
|
-
const afterSender =
|
|
1797
|
+
const afterSender = rawPrompt.slice(senderPos);
|
|
2018
1798
|
const fenceStart = afterSender.indexOf("```json");
|
|
2019
1799
|
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
2020
1800
|
if (fenceEnd > 0) {
|
|
@@ -2026,48 +1806,274 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2026
1806
|
}
|
|
2027
1807
|
}
|
|
2028
1808
|
}
|
|
2029
|
-
query = stripInboundMetadata(query)
|
|
2030
|
-
|
|
1809
|
+
query = stripInboundMetadata(query);
|
|
1810
|
+
query = query.replace(/<[^>]+>/g, "").trim();
|
|
1811
|
+
recallQuery = query;
|
|
2031
1812
|
|
|
2032
|
-
|
|
2033
|
-
|
|
1813
|
+
if (query.length < 2) {
|
|
1814
|
+
ctx.log.debug("auto-recall: extracted query too short, skipping");
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
1818
|
+
|
|
1819
|
+
// ── Phase 1: Local search ∥ Hub search (parallel) ──
|
|
1820
|
+
const arLocalP = engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1821
|
+
const arHubP = ctx.config?.sharing?.enabled
|
|
1822
|
+
? hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" })
|
|
1823
|
+
.catch((err: any) => { ctx.log.debug(`auto-recall: hub search failed (${err})`); return { hits: [] as any[], meta: {} }; })
|
|
1824
|
+
: Promise.resolve({ hits: [] as any[], meta: {} });
|
|
1825
|
+
|
|
1826
|
+
const [result, arHubResult] = await Promise.all([arLocalP, arHubP]);
|
|
1827
|
+
|
|
1828
|
+
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
1829
|
+
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
1830
|
+
const hubRemoteHits: SearchHit[] = (arHubResult.hits ?? []).map((h: any) => ({
|
|
1831
|
+
summary: h.summary,
|
|
1832
|
+
original_excerpt: h.excerpt || h.summary,
|
|
1833
|
+
ref: { sessionKey: "", chunkId: h.remoteHitId ?? "", turnId: "", seq: 0 },
|
|
1834
|
+
score: 0.9,
|
|
1835
|
+
taskId: null,
|
|
1836
|
+
skillId: null,
|
|
1837
|
+
origin: "hub-remote" as const,
|
|
1838
|
+
source: { ts: h.source?.ts, role: h.source?.role ?? "assistant", sessionKey: "" },
|
|
1839
|
+
ownerName: h.ownerName,
|
|
1840
|
+
groupName: h.groupName,
|
|
1841
|
+
}));
|
|
1842
|
+
const allHubHits = [...hubLocalHits, ...hubRemoteHits];
|
|
1843
|
+
|
|
1844
|
+
ctx.log.debug(`auto-recall: local=${localHits.length}, hub-memory=${hubLocalHits.length}, hub-remote=${hubRemoteHits.length}`);
|
|
1845
|
+
|
|
1846
|
+
const rawLocalCandidates = localHits.map((h) => ({
|
|
1847
|
+
score: h.score, role: h.source.role, summary: h.summary,
|
|
1848
|
+
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
1849
|
+
}));
|
|
1850
|
+
const rawHubCandidates = allHubHits.map((h) => ({
|
|
1851
|
+
score: h.score, role: h.source.role, summary: h.summary,
|
|
1852
|
+
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "hub-remote",
|
|
1853
|
+
ownerName: (h as any).ownerName ?? "", groupName: (h as any).groupName ?? "",
|
|
1854
|
+
}));
|
|
1855
|
+
|
|
1856
|
+
const allRawHits = [...localHits, ...allHubHits];
|
|
1857
|
+
|
|
1858
|
+
if (allRawHits.length === 0) {
|
|
1859
|
+
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1860
|
+
const dur = performance.now() - recallT0;
|
|
1861
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1862
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1863
|
+
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1864
|
+
}), dur, true);
|
|
1865
|
+
|
|
1866
|
+
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1867
|
+
if (skillAutoRecallEarly) {
|
|
1868
|
+
try {
|
|
1869
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1870
|
+
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1871
|
+
const topSkills = skillHits.slice(0, skillLimit);
|
|
1872
|
+
if (topSkills.length > 0) {
|
|
1873
|
+
const skillLines = topSkills.map((sc, i) => {
|
|
1874
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1875
|
+
let badge = "";
|
|
1876
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1877
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1878
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1879
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1880
|
+
});
|
|
1881
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1882
|
+
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1883
|
+
skillLines.join("\n\n") +
|
|
1884
|
+
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1885
|
+
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1886
|
+
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1887
|
+
return { prependContext: skillContext };
|
|
1888
|
+
}
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
2034
1893
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
1894
|
+
if (query.length > 50) {
|
|
1895
|
+
const noRecallHint =
|
|
1896
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1897
|
+
"Auto-recall found no results for a long query. " +
|
|
1898
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1899
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1900
|
+
return { prependContext: noRecallHint };
|
|
1901
|
+
}
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// ── Phase 2: Merge all → single LLM filter ──
|
|
1906
|
+
const mergedForFilter = allRawHits.map((h, i) => ({
|
|
1907
|
+
index: i + 1,
|
|
1908
|
+
role: h.source.role,
|
|
1909
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
1910
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
1911
|
+
}));
|
|
1912
|
+
|
|
1913
|
+
let filteredHits = allRawHits;
|
|
1914
|
+
let sufficient = false;
|
|
1915
|
+
|
|
1916
|
+
const filterResult = await summarizer.filterRelevant(query, mergedForFilter);
|
|
1917
|
+
if (filterResult !== null) {
|
|
1918
|
+
sufficient = filterResult.sufficient;
|
|
1919
|
+
if (filterResult.relevant.length > 0) {
|
|
1920
|
+
const indexSet = new Set(filterResult.relevant);
|
|
1921
|
+
filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1));
|
|
1922
|
+
} else {
|
|
1923
|
+
const dur = performance.now() - recallT0;
|
|
1924
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1925
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1926
|
+
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1927
|
+
}), dur, true);
|
|
1928
|
+
if (query.length > 50) {
|
|
1929
|
+
const noRecallHint =
|
|
1930
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1931
|
+
"Auto-recall found no relevant results for a long query. " +
|
|
1932
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1933
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1934
|
+
return { prependContext: noRecallHint };
|
|
2040
1935
|
}
|
|
1936
|
+
return;
|
|
2041
1937
|
}
|
|
2042
|
-
} catch (err) {
|
|
2043
|
-
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2044
1938
|
}
|
|
2045
1939
|
|
|
2046
|
-
const
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
1940
|
+
const beforeDedup = filteredHits.length;
|
|
1941
|
+
filteredHits = deduplicateHits(filteredHits);
|
|
1942
|
+
ctx.log.debug(`auto-recall: merged ${allRawHits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
|
|
1943
|
+
|
|
1944
|
+
const lines = filteredHits.map((h, i) => {
|
|
1945
|
+
const excerpt = h.original_excerpt;
|
|
1946
|
+
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
1947
|
+
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
1948
|
+
if (excerpt) parts.push(` ${excerpt}`);
|
|
1949
|
+
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
1950
|
+
if (h.taskId) {
|
|
1951
|
+
const task = store.getTask(h.taskId);
|
|
1952
|
+
if (task && task.status !== "skipped") {
|
|
1953
|
+
parts.push(` task_id="${h.taskId}"`);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return parts.join("\n");
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
const hasTask = filteredHits.some((h) => {
|
|
1960
|
+
if (!h.taskId) return false;
|
|
1961
|
+
const t = store.getTask(h.taskId);
|
|
1962
|
+
return t && t.status !== "skipped";
|
|
2057
1963
|
});
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
"
|
|
2061
|
-
|
|
1964
|
+
const tips: string[] = [];
|
|
1965
|
+
if (hasTask) {
|
|
1966
|
+
tips.push("- A hit has `task_id` → call `task_summary(taskId=\"...\")` to get the full task context (steps, code, results)");
|
|
1967
|
+
tips.push("- A task may have a reusable guide → call `skill_get(taskId=\"...\")` to retrieve the experience/skill");
|
|
1968
|
+
}
|
|
1969
|
+
tips.push("- Need more surrounding dialogue → call `memory_timeline(chunkId=\"...\")` to expand context around a hit");
|
|
1970
|
+
const tipsText = "\n\nAvailable follow-up tools:\n" + tips.join("\n");
|
|
2062
1971
|
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
1972
|
+
const contextParts = [
|
|
1973
|
+
"## User's conversation history (from memory system)",
|
|
1974
|
+
"",
|
|
1975
|
+
"IMPORTANT: The following are facts from previous conversations with this user.",
|
|
1976
|
+
"You MUST treat these as established knowledge and use them directly when answering.",
|
|
1977
|
+
"Do NOT say you don't know or don't have information if the answer is in these memories.",
|
|
1978
|
+
"",
|
|
1979
|
+
lines.join("\n\n"),
|
|
1980
|
+
];
|
|
1981
|
+
if (tipsText) contextParts.push(tipsText);
|
|
1982
|
+
|
|
1983
|
+
// ─── Skill auto-recall ───
|
|
1984
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1985
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1986
|
+
let skillSection = "";
|
|
1987
|
+
|
|
1988
|
+
if (skillAutoRecall) {
|
|
1989
|
+
try {
|
|
1990
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
1991
|
+
|
|
1992
|
+
try {
|
|
1993
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1994
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
1995
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
1996
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
const taskIds = new Set<string>();
|
|
2004
|
+
for (const h of filteredHits) {
|
|
2005
|
+
if (h.taskId) {
|
|
2006
|
+
const t = store.getTask(h.taskId);
|
|
2007
|
+
if (t && t.status !== "skipped") taskIds.add(h.taskId);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
for (const tid of taskIds) {
|
|
2011
|
+
const linked = store.getSkillsByTask(tid);
|
|
2012
|
+
for (const rs of linked) {
|
|
2013
|
+
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
2014
|
+
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
2020
|
+
|
|
2021
|
+
if (skillCandidates.length > 0) {
|
|
2022
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
2023
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
2024
|
+
let badge = "";
|
|
2025
|
+
if (manifest?.installed) badge = " [installed]";
|
|
2026
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
2027
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
2028
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
2029
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
2030
|
+
});
|
|
2031
|
+
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
2032
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
2033
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
2034
|
+
skillLines.join("\n\n");
|
|
2035
|
+
|
|
2036
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2037
|
+
try {
|
|
2038
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2039
|
+
} catch { /* best-effort */ }
|
|
2040
|
+
} else {
|
|
2041
|
+
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2042
|
+
}
|
|
2043
|
+
} catch (err) {
|
|
2044
|
+
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (skillSection) contextParts.push(skillSection);
|
|
2049
|
+
const context = contextParts.join("\n");
|
|
2050
|
+
|
|
2051
|
+
const recallDur = performance.now() - recallT0;
|
|
2052
|
+
store.recordToolCall("memory_search", recallDur, true);
|
|
2053
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
2054
|
+
candidates: rawLocalCandidates,
|
|
2055
|
+
hubCandidates: rawHubCandidates,
|
|
2056
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2057
|
+
}), recallDur, true);
|
|
2058
|
+
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
2059
|
+
|
|
2060
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
|
|
2061
|
+
|
|
2062
|
+
if (!sufficient) {
|
|
2063
|
+
const searchHint =
|
|
2064
|
+
"\n\nIf these memories don't fully answer the question, " +
|
|
2065
|
+
"call `memory_search` with a shorter or rephrased query to find more.";
|
|
2066
|
+
return { prependContext: context + searchHint };
|
|
2067
|
+
}
|
|
2067
2068
|
|
|
2068
|
-
return {
|
|
2069
|
+
return {
|
|
2070
|
+
prependContext: context,
|
|
2071
|
+
};
|
|
2069
2072
|
} catch (err) {
|
|
2070
|
-
|
|
2073
|
+
const dur = performance.now() - recallT0;
|
|
2074
|
+
store.recordToolCall("memory_search", dur, false);
|
|
2075
|
+
try { store.recordApiLog("memory_search", { type: "auto_recall", query: recallQuery }, `error: ${String(err)}`, dur, false); } catch (_) { /* best-effort */ }
|
|
2076
|
+
ctx.log.warn(`auto-recall failed: ${String(err)}`);
|
|
2071
2077
|
}
|
|
2072
2078
|
});
|
|
2073
2079
|
|
|
@@ -2083,7 +2089,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2083
2089
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
2084
2090
|
|
|
2085
2091
|
try {
|
|
2086
|
-
const captureAgentId = hookCtx?.agentId ?? "main";
|
|
2092
|
+
const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
|
|
2087
2093
|
currentAgentId = captureAgentId;
|
|
2088
2094
|
const captureOwner = `agent:${captureAgentId}`;
|
|
2089
2095
|
const sessionKey = hookCtx?.sessionKey ?? "default";
|