@mingxy/cerebro 1.15.3 → 1.15.5
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/.omo/evidence/f1-verification.txt +44 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +294 -224
- package/dist/hooks.js.map +1 -1
- package/dist/keywords.d.ts +1 -1
- package/dist/keywords.d.ts.map +1 -1
- package/dist/keywords.js +4 -32
- package/dist/keywords.js.map +1 -1
- package/package.json +1 -1
- package/src/hooks.ts +331 -254
- package/src/keywords.ts +4 -34
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveAgentPolicy } from "./config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { detectSaveKeyword, KEYWORD_NUDGE } from "./keywords.js";
|
|
3
3
|
import { logDebug, logInfo, logError as logErr } from "./logger.js";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { stripPrivateContent } from "./privacy.js";
|
|
@@ -157,13 +157,15 @@ function extractUserRequest(content) {
|
|
|
157
157
|
}
|
|
158
158
|
return text;
|
|
159
159
|
}
|
|
160
|
-
const
|
|
160
|
+
const saveKeywordDetectedSessions = new Set();
|
|
161
161
|
const injectedMemoryIds = new Map();
|
|
162
162
|
const firstMessages = new Map();
|
|
163
163
|
const sessionMessages = new Map();
|
|
164
164
|
const profileInjectedSessions = new Map();
|
|
165
165
|
const injectedSessions = new Set();
|
|
166
166
|
const compactingSummaryCooldown = new Map();
|
|
167
|
+
// Per-session async cache for fire-and-forget recall results
|
|
168
|
+
const recallCache = new Map();
|
|
167
169
|
function hashString(str) {
|
|
168
170
|
let hash = 0;
|
|
169
171
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -535,9 +537,9 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
|
|
|
535
537
|
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
536
538
|
}
|
|
537
539
|
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
538
|
-
if (
|
|
540
|
+
if (saveKeywordDetectedSessions.has(input.sessionID)) {
|
|
539
541
|
appendToSystem(output.system, KEYWORD_NUDGE);
|
|
540
|
-
|
|
542
|
+
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
541
543
|
}
|
|
542
544
|
}
|
|
543
545
|
catch (err) {
|
|
@@ -564,6 +566,28 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
|
|
|
564
566
|
}
|
|
565
567
|
};
|
|
566
568
|
}
|
|
569
|
+
function buildProfileBlock(profile) {
|
|
570
|
+
const prefs = (profile?.static_facts ?? [])
|
|
571
|
+
.filter((sf) => {
|
|
572
|
+
const t = sf.tags ?? [];
|
|
573
|
+
return t.includes("preferences");
|
|
574
|
+
})
|
|
575
|
+
.map((sf) => sf.l2_content ?? sf.content ?? "")
|
|
576
|
+
.filter(Boolean);
|
|
577
|
+
const profileLines = prefs.length > 0
|
|
578
|
+
? prefs.map((c) => ` · ${c}`).join("\n")
|
|
579
|
+
: " · (preferences queuing, will populate on next refresh)";
|
|
580
|
+
const block = [
|
|
581
|
+
"<cerebro-profile>",
|
|
582
|
+
profileLines,
|
|
583
|
+
"</cerebro-profile>",
|
|
584
|
+
].join("\n");
|
|
585
|
+
const p = profile;
|
|
586
|
+
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
587
|
+
const staticCount = p?.static_facts?.length ?? 0;
|
|
588
|
+
const countText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
589
|
+
return { block, countText };
|
|
590
|
+
}
|
|
567
591
|
export function memoryInjectionHook(client, containerTags, tui, config = {}, getAgentName, directory) {
|
|
568
592
|
const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
|
|
569
593
|
const maxRecallResults = config.recall?.maxRecallResults ?? 10;
|
|
@@ -585,51 +609,11 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
|
|
|
585
609
|
const policy = resolveAgentPolicy(agentId, config);
|
|
586
610
|
if (policy === "none")
|
|
587
611
|
return;
|
|
588
|
-
const
|
|
589
|
-
const isKeywordTriggered = keywordDetectedSessions.has(input.sessionID);
|
|
590
|
-
if (!isFirstInjection && !isKeywordTriggered)
|
|
591
|
-
return;
|
|
612
|
+
const isSaveKeyword = saveKeywordDetectedSessions.has(input.sessionID);
|
|
592
613
|
try {
|
|
593
|
-
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy,
|
|
614
|
+
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy, isSaveKeyword, similarityThreshold, maxRecallResults });
|
|
594
615
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
595
616
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
596
|
-
// --- Profile Fetch ---
|
|
597
|
-
const profile = await client.getProfile();
|
|
598
|
-
let profileInjected = false;
|
|
599
|
-
let profileCountText = "";
|
|
600
|
-
let profileBlock = "";
|
|
601
|
-
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
602
|
-
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
|
|
603
|
-
const profileIsFirstInjection = !lastInjected;
|
|
604
|
-
if (profile && ttlExpired) {
|
|
605
|
-
const prefs = (profile?.static_facts ?? [])
|
|
606
|
-
.filter((sf) => {
|
|
607
|
-
const t = sf.tags ?? [];
|
|
608
|
-
return t.includes("preferences");
|
|
609
|
-
})
|
|
610
|
-
.map((sf) => sf.l2_content ?? sf.content ?? "")
|
|
611
|
-
.filter(Boolean);
|
|
612
|
-
const profileLines = prefs.length > 0
|
|
613
|
-
? prefs.map((c) => ` · ${c}`).join("\n")
|
|
614
|
-
: " · (preferences queuing, will populate on next refresh)";
|
|
615
|
-
profileBlock = [
|
|
616
|
-
"<cerebro-profile>",
|
|
617
|
-
profileLines,
|
|
618
|
-
"</cerebro-profile>",
|
|
619
|
-
].join("\n");
|
|
620
|
-
profileInjected = true;
|
|
621
|
-
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
622
|
-
const p = profile;
|
|
623
|
-
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
624
|
-
const staticCount = p?.static_facts?.length ?? 0;
|
|
625
|
-
profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
626
|
-
if (profileIsFirstInjection) {
|
|
627
|
-
logDebug("memoryInjectionHook profile ready (first)", { dynamicCount, staticCount });
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
logDebug("memoryInjectionHook profile ready (TTL)", { dynamicCount, staticCount });
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
617
|
if (userMessages.length === 0) {
|
|
634
618
|
logDebug("memoryInjectionHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
|
|
635
619
|
return;
|
|
@@ -637,7 +621,7 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
|
|
|
637
621
|
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
638
622
|
const query_text = extractUserRequest(rawQuery);
|
|
639
623
|
if (!query_text) {
|
|
640
|
-
logDebug("memoryInjectionHook filtered system injection
|
|
624
|
+
logDebug("memoryInjectionHook filtered system injection", { rawQueryPrefix: rawQuery.slice(0, 60) });
|
|
641
625
|
return;
|
|
642
626
|
}
|
|
643
627
|
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
@@ -648,28 +632,239 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
|
|
|
648
632
|
return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
|
|
649
633
|
})
|
|
650
634
|
: undefined;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
635
|
+
// ========== Phase A: synchronous path (zero await) ==========
|
|
636
|
+
const cached = recallCache.get(input.sessionID);
|
|
637
|
+
let profileBlock = "";
|
|
638
|
+
let profileInjected = false;
|
|
639
|
+
let profileCountText = "";
|
|
640
|
+
if (cached) {
|
|
641
|
+
// Phase A: 只读 profileBlock,不更新 TTL(TTL 管理完全由 Phase B 负责)
|
|
642
|
+
if (cached.profileBlock) {
|
|
643
|
+
profileBlock = cached.profileBlock;
|
|
644
|
+
profileInjected = true;
|
|
645
|
+
profileCountText = cached.profileData?.countText ?? "";
|
|
646
|
+
}
|
|
647
|
+
const shouldRecallRes = cached.recallResult;
|
|
648
|
+
if (!shouldRecallRes.should_recall) {
|
|
649
|
+
const partsToInject = [];
|
|
650
|
+
if (profileBlock)
|
|
651
|
+
partsToInject.push(profileBlock);
|
|
652
|
+
if (partsToInject.length > 0) {
|
|
653
|
+
const injectText = partsToInject.join("\n\n");
|
|
654
|
+
const contextPart = {
|
|
655
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
656
|
+
sessionID: input.sessionID,
|
|
657
|
+
messageID: output.message.id,
|
|
658
|
+
type: "text",
|
|
659
|
+
text: injectText,
|
|
660
|
+
synthetic: true,
|
|
661
|
+
};
|
|
662
|
+
output.parts.unshift(contextPart);
|
|
663
|
+
logDebug("memoryInjectionHook profile injected from cache (no-recall)", { sessionId: input.sessionID });
|
|
664
|
+
}
|
|
665
|
+
injectedSessions.add(input.sessionID);
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const results = shouldRecallRes.memories ?? [];
|
|
669
|
+
const clustered = shouldRecallRes.clustered;
|
|
670
|
+
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
|
|
671
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
672
|
+
logDebug("memoryInjectionHook dedup (cached)", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
673
|
+
if (newResults.length === 0) {
|
|
674
|
+
const partsToInject = [];
|
|
675
|
+
if (profileBlock)
|
|
676
|
+
partsToInject.push(profileBlock);
|
|
677
|
+
if (partsToInject.length > 0) {
|
|
678
|
+
const injectText = partsToInject.join("\n\n");
|
|
679
|
+
const contextPart = {
|
|
680
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
681
|
+
sessionID: input.sessionID,
|
|
682
|
+
messageID: output.message.id,
|
|
683
|
+
type: "text",
|
|
684
|
+
text: injectText,
|
|
685
|
+
synthetic: true,
|
|
686
|
+
};
|
|
687
|
+
output.parts.unshift(contextPart);
|
|
688
|
+
logDebug("memoryInjectionHook profile injected from cache (dedup)", { sessionId: input.sessionID });
|
|
689
|
+
}
|
|
690
|
+
injectedSessions.add(input.sessionID);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
694
|
+
const budgetRemaining = maxContentChars - profileChars;
|
|
695
|
+
const itemCount = clustered
|
|
696
|
+
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
697
|
+
: newResults.length;
|
|
698
|
+
const dynamicMaxContentLength = itemCount > 0
|
|
699
|
+
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
700
|
+
: maxContentLength;
|
|
701
|
+
const block = clustered
|
|
702
|
+
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
703
|
+
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
704
|
+
const partsToInject = [];
|
|
705
|
+
if (block)
|
|
706
|
+
partsToInject.push(block);
|
|
707
|
+
if (block)
|
|
708
|
+
partsToInject.push(FETCH_POLICY);
|
|
709
|
+
if (profileBlock)
|
|
710
|
+
partsToInject.push(profileBlock);
|
|
711
|
+
if (isSaveKeyword)
|
|
712
|
+
partsToInject.push(KEYWORD_NUDGE);
|
|
713
|
+
if (partsToInject.length > 0) {
|
|
714
|
+
const injectText = partsToInject.join("\n\n");
|
|
715
|
+
const contextPart = {
|
|
716
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
717
|
+
sessionID: input.sessionID,
|
|
718
|
+
messageID: output.message.id,
|
|
719
|
+
type: "text",
|
|
720
|
+
text: injectText,
|
|
721
|
+
synthetic: true,
|
|
722
|
+
};
|
|
723
|
+
output.parts.unshift(contextPart);
|
|
724
|
+
logDebug("memoryInjectionHook block injected from cache", {
|
|
725
|
+
sessionId: input.sessionID,
|
|
726
|
+
injectTextLen: injectText.length,
|
|
727
|
+
blockPreview: block?.slice(0, 200),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
injectedSessions.add(input.sessionID);
|
|
731
|
+
if (isSaveKeyword) {
|
|
732
|
+
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
733
|
+
}
|
|
734
|
+
const newIds = newResults.map((r) => r.memory.id);
|
|
735
|
+
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
736
|
+
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
737
|
+
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
738
|
+
const memOther = newResults.length - memDynamic - memStatic;
|
|
739
|
+
let memCountMsg = "";
|
|
740
|
+
if (memDynamic > 0)
|
|
741
|
+
memCountMsg += `Dynamic(${memDynamic}) `;
|
|
742
|
+
if (memStatic > 0)
|
|
743
|
+
memCountMsg += `Static(${memStatic}) `;
|
|
744
|
+
if (memOther > 0)
|
|
745
|
+
memCountMsg += `Other(${memOther}) `;
|
|
746
|
+
const categories = categorize(newResults);
|
|
747
|
+
const catSummary = Array.from(categories.entries())
|
|
748
|
+
.map(([label, items]) => `${label}(${items.length})`)
|
|
749
|
+
.join(" · ");
|
|
750
|
+
let toastTitle;
|
|
751
|
+
let toastMessage;
|
|
752
|
+
if (clustered) {
|
|
753
|
+
const clusterCount = clustered.cluster_summaries.length;
|
|
754
|
+
const standaloneCount = clustered.standalone_memories.length;
|
|
755
|
+
toastTitle = `🧠 Context Injected · ${clusterCount} 主题簇${standaloneCount > 0 ? ` · ${standaloneCount} 补充` : ""}`;
|
|
756
|
+
toastMessage = profileInjected
|
|
757
|
+
? `Profile: ${profileCountText} · 聚合记忆展示`
|
|
758
|
+
: `聚合记忆展示`;
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
|
|
762
|
+
toastMessage = profileInjected
|
|
763
|
+
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
764
|
+
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
765
|
+
}
|
|
766
|
+
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
logDebug("memoryInjectionHook cache hit, injection complete", { sessionId: input.sessionID });
|
|
664
770
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
771
|
+
else {
|
|
772
|
+
logDebug("memoryInjectionHook cache miss, first message in session", { sessionId: input.sessionID });
|
|
773
|
+
}
|
|
774
|
+
// ========== Phase B: fire-and-forget async fetch for NEXT round ==========
|
|
775
|
+
const bgSessionId = input.sessionID;
|
|
776
|
+
const bgQueryText = query_text;
|
|
777
|
+
const bgLastQueryText = last_query_text;
|
|
778
|
+
const bgConversationContext = conversationContext;
|
|
779
|
+
const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
|
|
780
|
+
const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
|
|
781
|
+
Promise.allSettled([
|
|
782
|
+
client.getProfile(),
|
|
783
|
+
client.shouldRecall(bgQueryText, bgLastQueryText, bgSessionId, similarityThreshold, maxRecallResults, bgProjectTags, bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined, {
|
|
784
|
+
fetch_multiplier: fetchMultiplier,
|
|
785
|
+
topk_cap_multiplier: topkCapMultiplier,
|
|
786
|
+
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
787
|
+
mmr_penalty_factor: mmrPenaltyFactor,
|
|
788
|
+
phase2_multiplier: phase2Multiplier,
|
|
789
|
+
llm_max_eval: llmMaxEval,
|
|
790
|
+
refine_strategy: refineStrategy,
|
|
791
|
+
refine_medium_chars: refineMediumChars,
|
|
792
|
+
}, bgDirectory),
|
|
793
|
+
])
|
|
794
|
+
.then(([profileRes, recallRes]) => {
|
|
795
|
+
if (recallRes.status === 'rejected') {
|
|
796
|
+
logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
|
|
800
|
+
const shouldRecallRes = recallRes.value;
|
|
801
|
+
if (!shouldRecallRes) {
|
|
802
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
logDebug("memoryInjectionHook background fetch complete", {
|
|
806
|
+
sessionId: bgSessionId,
|
|
807
|
+
shouldRecall: shouldRecallRes.should_recall,
|
|
808
|
+
confidence: shouldRecallRes.confidence,
|
|
809
|
+
memCount: shouldRecallRes.memories?.length ?? 0,
|
|
810
|
+
});
|
|
811
|
+
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
812
|
+
logErr("memoryInjectionHook shouldRecall returned incomplete data", {
|
|
813
|
+
shouldRecall: shouldRecallRes.should_recall,
|
|
814
|
+
hasMemories: !!shouldRecallRes.memories,
|
|
815
|
+
});
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
let bgProfileBlock = "";
|
|
819
|
+
let bgProfileCountText = "";
|
|
820
|
+
let bgProfileInjected = false;
|
|
821
|
+
if (profile) {
|
|
822
|
+
const lastInjected = profileInjectedSessions.get(bgSessionId);
|
|
823
|
+
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
|
|
824
|
+
if (ttlExpired) {
|
|
825
|
+
const built = buildProfileBlock(profile);
|
|
826
|
+
if (built) {
|
|
827
|
+
bgProfileBlock = built.block;
|
|
828
|
+
bgProfileCountText = built.countText;
|
|
829
|
+
bgProfileInjected = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
recallCache.set(bgSessionId, {
|
|
834
|
+
profileBlock: bgProfileBlock,
|
|
835
|
+
recallResult: shouldRecallRes,
|
|
836
|
+
profileData: { countText: bgProfileCountText },
|
|
837
|
+
timestamp: Date.now(),
|
|
838
|
+
});
|
|
839
|
+
if (recallCache.size > 50) {
|
|
840
|
+
let oldestKey = null;
|
|
841
|
+
let oldestTime = Infinity;
|
|
842
|
+
for (const [k, v] of recallCache) {
|
|
843
|
+
if (v.timestamp < oldestTime) {
|
|
844
|
+
oldestTime = v.timestamp;
|
|
845
|
+
oldestKey = k;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (oldestKey)
|
|
849
|
+
recallCache.delete(oldestKey);
|
|
850
|
+
}
|
|
851
|
+
if (shouldRecallRes.should_recall) {
|
|
852
|
+
const results = shouldRecallRes.memories ?? [];
|
|
853
|
+
const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set();
|
|
854
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
855
|
+
if (newResults.length > 0) {
|
|
856
|
+
const newIds = newResults.map((r) => r.memory.id);
|
|
857
|
+
injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
|
|
858
|
+
}
|
|
859
|
+
const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
|
|
860
|
+
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
861
|
+
const maxScore = storedMemoryIds.length > 0
|
|
862
|
+
? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
|
|
863
|
+
: 0;
|
|
864
|
+
const bgBlock = shouldRecallRes.clustered
|
|
865
|
+
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
866
|
+
: buildContextBlock(newResults, maxContentLength);
|
|
867
|
+
const bgInjectedContent = bgBlock ?? undefined;
|
|
673
868
|
const items = [
|
|
674
869
|
...(shouldRecallRes.memories?.map((r) => ({
|
|
675
870
|
memory_id: r.memory.id,
|
|
@@ -686,169 +881,40 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
|
|
|
686
881
|
is_kept: false,
|
|
687
882
|
})) ?? []),
|
|
688
883
|
];
|
|
689
|
-
|
|
690
|
-
session_id:
|
|
884
|
+
client.createRecallEvent({
|
|
885
|
+
session_id: bgSessionId,
|
|
691
886
|
recall_type: "auto",
|
|
692
|
-
query_text,
|
|
887
|
+
query_text: bgQueryText,
|
|
693
888
|
max_score: maxScore,
|
|
694
889
|
llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
695
|
-
profile_injected:
|
|
696
|
-
kept_count:
|
|
697
|
-
discarded_count:
|
|
698
|
-
injected_count:
|
|
699
|
-
profile_content:
|
|
700
|
-
injected_content:
|
|
890
|
+
profile_injected: bgProfileInjected,
|
|
891
|
+
kept_count: storedMemoryIds.length,
|
|
892
|
+
discarded_count: storedDiscardedIds.length,
|
|
893
|
+
injected_count: newResults.length,
|
|
894
|
+
profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
|
|
895
|
+
injected_content: bgInjectedContent,
|
|
701
896
|
items: items.length > 0 ? items : undefined,
|
|
897
|
+
}).catch((e) => {
|
|
898
|
+
logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
|
|
702
899
|
});
|
|
703
|
-
return result?.event_id;
|
|
704
900
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const contextPart = {
|
|
718
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
719
|
-
sessionID: input.sessionID,
|
|
720
|
-
messageID: output.message.id,
|
|
721
|
-
type: "text",
|
|
722
|
-
text: injectText,
|
|
723
|
-
synthetic: true,
|
|
724
|
-
};
|
|
725
|
-
output.parts.unshift(contextPart);
|
|
726
|
-
logDebug("memoryInjectionHook profile injected (no-recall path)", { sessionId: input.sessionID });
|
|
727
|
-
}
|
|
728
|
-
injectedSessions.add(input.sessionID);
|
|
729
|
-
if (profileInjected && profileIsFirstInjection) {
|
|
730
|
-
await createEventAndReturn(0, 0, 0);
|
|
731
|
-
showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
|
|
732
|
-
}
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const results = shouldRecallRes.memories ?? [];
|
|
736
|
-
const clustered = shouldRecallRes.clustered;
|
|
737
|
-
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
|
|
738
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
739
|
-
logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
740
|
-
// --- dedup path: inject profile only ---
|
|
741
|
-
if (newResults.length === 0) {
|
|
742
|
-
const partsToInject = [];
|
|
743
|
-
if (profileBlock)
|
|
744
|
-
partsToInject.push(profileBlock);
|
|
745
|
-
if (partsToInject.length > 0) {
|
|
746
|
-
const injectText = partsToInject.join("\n\n");
|
|
747
|
-
const contextPart = {
|
|
748
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
749
|
-
sessionID: input.sessionID,
|
|
750
|
-
messageID: output.message.id,
|
|
751
|
-
type: "text",
|
|
752
|
-
text: injectText,
|
|
753
|
-
synthetic: true,
|
|
754
|
-
};
|
|
755
|
-
output.parts.unshift(contextPart);
|
|
756
|
-
logDebug("memoryInjectionHook profile injected (dedup path)", { sessionId: input.sessionID });
|
|
901
|
+
})
|
|
902
|
+
.catch((err) => {
|
|
903
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
904
|
+
logErr("memoryInjectionHook background fetch failed", { error: errMsg });
|
|
905
|
+
if (errMsg.includes("[cerebro]")) {
|
|
906
|
+
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
907
|
+
if (cleanMsg.startsWith("500")) {
|
|
908
|
+
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
909
|
+
}
|
|
910
|
+
else if (cleanMsg.includes("timed out")) {
|
|
911
|
+
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
912
|
+
}
|
|
757
913
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
|
|
914
|
+
else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
915
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
761
916
|
}
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
// --- Token Budget Calculation ---
|
|
765
|
-
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
766
|
-
const budgetRemaining = maxContentChars - profileChars;
|
|
767
|
-
if (budgetRemaining < 0) {
|
|
768
|
-
logDebug("memoryInjectionHook budget overflow", { profileChars, maxContentChars, deficit: -budgetRemaining });
|
|
769
|
-
}
|
|
770
|
-
const itemCount = clustered
|
|
771
|
-
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
772
|
-
: newResults.length;
|
|
773
|
-
const dynamicMaxContentLength = itemCount > 0
|
|
774
|
-
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
775
|
-
: maxContentLength;
|
|
776
|
-
logDebug("memoryInjectionHook budget", {
|
|
777
|
-
maxContentChars, profileChars, budgetRemaining, itemCount,
|
|
778
|
-
configuredMax: maxContentLength, dynamicMax: dynamicMaxContentLength,
|
|
779
917
|
});
|
|
780
|
-
const block = clustered
|
|
781
|
-
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
782
|
-
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
783
|
-
// ★★★ Core change: inject via output.parts.unshift + synthetic:true ★★★
|
|
784
|
-
const partsToInject = [];
|
|
785
|
-
if (profileBlock)
|
|
786
|
-
partsToInject.push(profileBlock);
|
|
787
|
-
if (block)
|
|
788
|
-
partsToInject.push(block);
|
|
789
|
-
if (block)
|
|
790
|
-
partsToInject.push(FETCH_POLICY);
|
|
791
|
-
if (isKeywordTriggered)
|
|
792
|
-
partsToInject.push(KEYWORD_NUDGE);
|
|
793
|
-
if (partsToInject.length > 0) {
|
|
794
|
-
const injectText = partsToInject.join("\n\n");
|
|
795
|
-
const contextPart = {
|
|
796
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
797
|
-
sessionID: input.sessionID,
|
|
798
|
-
messageID: output.message.id,
|
|
799
|
-
type: "text",
|
|
800
|
-
text: injectText,
|
|
801
|
-
synthetic: true,
|
|
802
|
-
};
|
|
803
|
-
output.parts.unshift(contextPart);
|
|
804
|
-
logDebug("memoryInjectionHook block injected to output.parts", {
|
|
805
|
-
sessionId: input.sessionID,
|
|
806
|
-
injectTextLen: injectText.length,
|
|
807
|
-
blockPreview: block?.slice(0, 200),
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
logDebug("memoryInjectionHook no content to inject", { sessionId: input.sessionID });
|
|
812
|
-
}
|
|
813
|
-
injectedSessions.add(input.sessionID);
|
|
814
|
-
if (isKeywordTriggered) {
|
|
815
|
-
keywordDetectedSessions.delete(input.sessionID);
|
|
816
|
-
}
|
|
817
|
-
const newIds = newResults.map((r) => r.memory.id);
|
|
818
|
-
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
819
|
-
logDebug("memoryInjectionHook injection complete", { newIds: newIds.length, clustered: !!clustered, sessionId: input.sessionID });
|
|
820
|
-
await createEventAndReturn(newResults.length, storedMemoryIds.length, storedDiscardedIds.length, block || undefined);
|
|
821
|
-
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
822
|
-
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
823
|
-
const memOther = newResults.length - memDynamic - memStatic;
|
|
824
|
-
let memCountMsg = "";
|
|
825
|
-
if (memDynamic > 0)
|
|
826
|
-
memCountMsg += `Dynamic(${memDynamic}) `;
|
|
827
|
-
if (memStatic > 0)
|
|
828
|
-
memCountMsg += `Static(${memStatic}) `;
|
|
829
|
-
if (memOther > 0)
|
|
830
|
-
memCountMsg += `Other(${memOther}) `;
|
|
831
|
-
const categories = categorize(newResults);
|
|
832
|
-
const catSummary = Array.from(categories.entries())
|
|
833
|
-
.map(([label, items]) => `${label}(${items.length})`)
|
|
834
|
-
.join(" · ");
|
|
835
|
-
let toastTitle;
|
|
836
|
-
let toastMessage;
|
|
837
|
-
if (clustered) {
|
|
838
|
-
const clusterCount = clustered.cluster_summaries.length;
|
|
839
|
-
const standaloneCount = clustered.standalone_memories.length;
|
|
840
|
-
toastTitle = `🧠 Context Injected · ${clusterCount} 主题簇${standaloneCount > 0 ? ` · ${standaloneCount} 补充` : ""}`;
|
|
841
|
-
toastMessage = profileInjected
|
|
842
|
-
? `Profile: ${profileCountText} · 聚合记忆展示`
|
|
843
|
-
: `聚合记忆展示`;
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
|
|
847
|
-
toastMessage = profileInjected
|
|
848
|
-
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
849
|
-
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
850
|
-
}
|
|
851
|
-
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
852
918
|
}
|
|
853
919
|
catch (err) {
|
|
854
920
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -885,8 +951,8 @@ export function keywordDetectionHook(_client, _containerTags, threshold, _tui, _
|
|
|
885
951
|
if (!firstMessages.has(input.sessionID)) {
|
|
886
952
|
firstMessages.set(input.sessionID, textContent);
|
|
887
953
|
}
|
|
888
|
-
if (
|
|
889
|
-
|
|
954
|
+
if (detectSaveKeyword(textContent)) {
|
|
955
|
+
saveKeywordDetectedSessions.add(input.sessionID);
|
|
890
956
|
logDebug("keywordDetectionHook triggered", { sessionId: input.sessionID });
|
|
891
957
|
}
|
|
892
958
|
const policy = resolveAgentPolicy(effectiveAgentId, config);
|
|
@@ -998,6 +1064,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
|
|
|
998
1064
|
if (input.sessionID) {
|
|
999
1065
|
sessionMessages.delete(input.sessionID);
|
|
1000
1066
|
profileInjectedSessions.delete(input.sessionID);
|
|
1067
|
+
recallCache.delete(input.sessionID);
|
|
1001
1068
|
firstMessages.delete(input.sessionID);
|
|
1002
1069
|
}
|
|
1003
1070
|
return;
|
|
@@ -1027,6 +1094,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
|
|
|
1027
1094
|
if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
|
|
1028
1095
|
sessionMessages.delete(input.sessionID);
|
|
1029
1096
|
profileInjectedSessions.delete(input.sessionID);
|
|
1097
|
+
recallCache.delete(input.sessionID);
|
|
1030
1098
|
firstMessages.delete(input.sessionID);
|
|
1031
1099
|
}
|
|
1032
1100
|
else {
|
|
@@ -1058,7 +1126,9 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
|
|
|
1058
1126
|
}
|
|
1059
1127
|
// Cleanup tracked messages regardless of ingest result
|
|
1060
1128
|
sessionMessages.delete(input.sessionID);
|
|
1129
|
+
injectedSessions.delete(input.sessionID);
|
|
1061
1130
|
profileInjectedSessions.delete(input.sessionID);
|
|
1131
|
+
recallCache.delete(input.sessionID);
|
|
1062
1132
|
firstMessages.delete(input.sessionID);
|
|
1063
1133
|
if (input.sessionID) {
|
|
1064
1134
|
const deleted = pendingToolCalls.delete(input.sessionID);
|