@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/src/hooks.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Model, UserMessage, Part } from "@opencode-ai/sdk";
|
|
2
|
-
import type { CerebroClient, SearchResult } from "./client.js";
|
|
2
|
+
import type { CerebroClient, SearchResult, ShouldRecallResponse } from "./client.js";
|
|
3
3
|
import { type OmemPluginConfig, resolveAgentPolicy } from "./config.js";
|
|
4
|
-
import {
|
|
4
|
+
import { detectSaveKeyword, KEYWORD_NUDGE } from "./keywords.js";
|
|
5
5
|
import { logDebug, logInfo, logError as logErr } from "./logger.js";
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
7
7
|
import { stripPrivateContent } from "./privacy.js";
|
|
@@ -167,7 +167,7 @@ function extractUserRequest(content: string): string {
|
|
|
167
167
|
return text;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
const
|
|
170
|
+
const saveKeywordDetectedSessions = new Set<string>();
|
|
171
171
|
const injectedMemoryIds = new Map<string, Set<string>>();
|
|
172
172
|
const firstMessages = new Map<string, string>();
|
|
173
173
|
const sessionMessages = new Map<string, Array<{ role: string; content: string }>>();
|
|
@@ -175,6 +175,14 @@ const profileInjectedSessions = new Map<string, number>();
|
|
|
175
175
|
const injectedSessions = new Set<string>();
|
|
176
176
|
const compactingSummaryCooldown = new Map<string, number>();
|
|
177
177
|
|
|
178
|
+
// Per-session async cache for fire-and-forget recall results
|
|
179
|
+
const recallCache = new Map<string, {
|
|
180
|
+
profileBlock: string;
|
|
181
|
+
recallResult: ShouldRecallResponse;
|
|
182
|
+
profileData: { countText: string };
|
|
183
|
+
timestamp: number;
|
|
184
|
+
}>();
|
|
185
|
+
|
|
178
186
|
function hashString(str: string): string {
|
|
179
187
|
let hash = 0;
|
|
180
188
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -590,9 +598,9 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
590
598
|
|
|
591
599
|
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
592
600
|
|
|
593
|
-
if (
|
|
601
|
+
if (saveKeywordDetectedSessions.has(input.sessionID)) {
|
|
594
602
|
appendToSystem(output.system, KEYWORD_NUDGE);
|
|
595
|
-
|
|
603
|
+
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
596
604
|
}
|
|
597
605
|
} catch (err) {
|
|
598
606
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -615,6 +623,29 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
615
623
|
};
|
|
616
624
|
}
|
|
617
625
|
|
|
626
|
+
function buildProfileBlock(profile: any): { block: string; countText: string } | null {
|
|
627
|
+
const prefs = ((profile as any)?.static_facts ?? [])
|
|
628
|
+
.filter((sf: any) => {
|
|
629
|
+
const t: string[] = sf.tags ?? [];
|
|
630
|
+
return t.includes("preferences");
|
|
631
|
+
})
|
|
632
|
+
.map((sf: any) => sf.l2_content ?? sf.content ?? "")
|
|
633
|
+
.filter(Boolean);
|
|
634
|
+
const profileLines = prefs.length > 0
|
|
635
|
+
? prefs.map((c: string) => ` · ${c}`).join("\n")
|
|
636
|
+
: " · (preferences queuing, will populate on next refresh)";
|
|
637
|
+
const block = [
|
|
638
|
+
"<cerebro-profile>",
|
|
639
|
+
profileLines,
|
|
640
|
+
"</cerebro-profile>",
|
|
641
|
+
].join("\n");
|
|
642
|
+
const p = profile as any;
|
|
643
|
+
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
644
|
+
const staticCount = p?.static_facts?.length ?? 0;
|
|
645
|
+
const countText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
646
|
+
return { block, countText };
|
|
647
|
+
}
|
|
648
|
+
|
|
618
649
|
export function memoryInjectionHook(
|
|
619
650
|
client: CerebroClient,
|
|
620
651
|
containerTags: string[],
|
|
@@ -647,52 +678,13 @@ export function memoryInjectionHook(
|
|
|
647
678
|
const policy = resolveAgentPolicy(agentId, config);
|
|
648
679
|
if (policy === "none") return;
|
|
649
680
|
|
|
650
|
-
const
|
|
651
|
-
const isKeywordTriggered = keywordDetectedSessions.has(input.sessionID);
|
|
652
|
-
if (!isFirstInjection && !isKeywordTriggered) return;
|
|
681
|
+
const isSaveKeyword = saveKeywordDetectedSessions.has(input.sessionID);
|
|
653
682
|
|
|
654
683
|
try {
|
|
655
|
-
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy,
|
|
684
|
+
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy, isSaveKeyword, similarityThreshold, maxRecallResults });
|
|
656
685
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
657
686
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
658
687
|
|
|
659
|
-
// --- Profile Fetch ---
|
|
660
|
-
const profile = await client.getProfile();
|
|
661
|
-
let profileInjected = false;
|
|
662
|
-
let profileCountText = "";
|
|
663
|
-
let profileBlock = "";
|
|
664
|
-
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
665
|
-
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
|
|
666
|
-
const profileIsFirstInjection = !lastInjected;
|
|
667
|
-
if (profile && ttlExpired) {
|
|
668
|
-
const prefs = ((profile as any)?.static_facts ?? [])
|
|
669
|
-
.filter((sf: any) => {
|
|
670
|
-
const t: string[] = sf.tags ?? [];
|
|
671
|
-
return t.includes("preferences");
|
|
672
|
-
})
|
|
673
|
-
.map((sf: any) => sf.l2_content ?? sf.content ?? "")
|
|
674
|
-
.filter(Boolean);
|
|
675
|
-
const profileLines = prefs.length > 0
|
|
676
|
-
? prefs.map((c: string) => ` · ${c}`).join("\n")
|
|
677
|
-
: " · (preferences queuing, will populate on next refresh)";
|
|
678
|
-
profileBlock = [
|
|
679
|
-
"<cerebro-profile>",
|
|
680
|
-
profileLines,
|
|
681
|
-
"</cerebro-profile>",
|
|
682
|
-
].join("\n");
|
|
683
|
-
profileInjected = true;
|
|
684
|
-
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
685
|
-
const p = profile as any;
|
|
686
|
-
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
687
|
-
const staticCount = p?.static_facts?.length ?? 0;
|
|
688
|
-
profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
689
|
-
if (profileIsFirstInjection) {
|
|
690
|
-
logDebug("memoryInjectionHook profile ready (first)", { dynamicCount, staticCount });
|
|
691
|
-
} else {
|
|
692
|
-
logDebug("memoryInjectionHook profile ready (TTL)", { dynamicCount, staticCount });
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
688
|
if (userMessages.length === 0) {
|
|
697
689
|
logDebug("memoryInjectionHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
|
|
698
690
|
return;
|
|
@@ -701,7 +693,7 @@ export function memoryInjectionHook(
|
|
|
701
693
|
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
702
694
|
const query_text = extractUserRequest(rawQuery);
|
|
703
695
|
if (!query_text) {
|
|
704
|
-
logDebug("memoryInjectionHook filtered system injection
|
|
696
|
+
logDebug("memoryInjectionHook filtered system injection", { rawQueryPrefix: rawQuery.slice(0, 60) });
|
|
705
697
|
return;
|
|
706
698
|
}
|
|
707
699
|
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
@@ -715,228 +707,309 @@ export function memoryInjectionHook(
|
|
|
715
707
|
})
|
|
716
708
|
: undefined;
|
|
717
709
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
{
|
|
724
|
-
fetch_multiplier: fetchMultiplier,
|
|
725
|
-
topk_cap_multiplier: topkCapMultiplier,
|
|
726
|
-
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
727
|
-
mmr_penalty_factor: mmrPenaltyFactor,
|
|
728
|
-
phase2_multiplier: phase2Multiplier,
|
|
729
|
-
llm_max_eval: llmMaxEval,
|
|
730
|
-
refine_strategy: refineStrategy,
|
|
731
|
-
refine_medium_chars: refineMediumChars,
|
|
732
|
-
},
|
|
733
|
-
directory || process.env.OMEM_PROJECT_DIR,
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
if (!shouldRecallRes) {
|
|
737
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
logDebug("memoryInjectionHook shouldRecall result", { shouldRecall: shouldRecallRes.should_recall, confidence: shouldRecallRes.confidence, memCount: shouldRecallRes.memories?.length ?? 0, discardedCount: shouldRecallRes.discarded?.length ?? 0, clustered: !!shouldRecallRes.clustered });
|
|
741
|
-
|
|
742
|
-
const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
|
|
743
|
-
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
744
|
-
const maxScore = storedMemoryIds.length > 0
|
|
745
|
-
? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
|
|
746
|
-
: 0;
|
|
747
|
-
|
|
748
|
-
const createEventAndReturn = async (
|
|
749
|
-
injectedCount: number,
|
|
750
|
-
keptCount: number,
|
|
751
|
-
discardedCount: number,
|
|
752
|
-
injectedContent?: string,
|
|
753
|
-
): Promise<string | undefined> => {
|
|
754
|
-
try {
|
|
755
|
-
const items = [
|
|
756
|
-
...(shouldRecallRes.memories?.map((r) => ({
|
|
757
|
-
memory_id: r.memory.id,
|
|
758
|
-
score: r.score,
|
|
759
|
-
refine_relevance: r.refine_relevance,
|
|
760
|
-
refine_reasoning: r.refine_reasoning,
|
|
761
|
-
is_kept: true,
|
|
762
|
-
})) ?? []),
|
|
763
|
-
...(shouldRecallRes.discarded?.map((d) => ({
|
|
764
|
-
memory_id: d.memory_id,
|
|
765
|
-
score: d.score,
|
|
766
|
-
refine_relevance: d.refine_relevance,
|
|
767
|
-
refine_reasoning: d.refine_reasoning,
|
|
768
|
-
is_kept: false,
|
|
769
|
-
})) ?? []),
|
|
770
|
-
];
|
|
771
|
-
const result = await client.createRecallEvent({
|
|
772
|
-
session_id: input.sessionID!,
|
|
773
|
-
recall_type: "auto",
|
|
774
|
-
query_text,
|
|
775
|
-
max_score: maxScore,
|
|
776
|
-
llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
777
|
-
profile_injected: profileInjected,
|
|
778
|
-
kept_count: keptCount,
|
|
779
|
-
discarded_count: discardedCount,
|
|
780
|
-
injected_count: injectedCount,
|
|
781
|
-
profile_content: profileInjected && profileBlock ? profileBlock : undefined,
|
|
782
|
-
injected_content: injectedContent,
|
|
783
|
-
items: items.length > 0 ? items : undefined,
|
|
784
|
-
});
|
|
785
|
-
return result?.event_id;
|
|
786
|
-
} catch (e) {
|
|
787
|
-
logErr("memoryInjectionHook createRecallEvent failed", { error: String(e) });
|
|
788
|
-
return undefined;
|
|
789
|
-
}
|
|
790
|
-
};
|
|
710
|
+
// ========== Phase A: synchronous path (zero await) ==========
|
|
711
|
+
const cached = recallCache.get(input.sessionID);
|
|
712
|
+
let profileBlock = "";
|
|
713
|
+
let profileInjected = false;
|
|
714
|
+
let profileCountText = "";
|
|
791
715
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const contextPart: Part = {
|
|
799
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
800
|
-
sessionID: input.sessionID,
|
|
801
|
-
messageID: output.message.id,
|
|
802
|
-
type: "text",
|
|
803
|
-
text: injectText,
|
|
804
|
-
synthetic: true,
|
|
805
|
-
};
|
|
806
|
-
output.parts.unshift(contextPart);
|
|
807
|
-
logDebug("memoryInjectionHook profile injected (no-recall path)", { sessionId: input.sessionID });
|
|
808
|
-
}
|
|
809
|
-
injectedSessions.add(input.sessionID);
|
|
810
|
-
if (profileInjected && profileIsFirstInjection) {
|
|
811
|
-
await createEventAndReturn(0, 0, 0);
|
|
812
|
-
showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
|
|
716
|
+
if (cached) {
|
|
717
|
+
// Phase A: 只读 profileBlock,不更新 TTL(TTL 管理完全由 Phase B 负责)
|
|
718
|
+
if (cached.profileBlock) {
|
|
719
|
+
profileBlock = cached.profileBlock;
|
|
720
|
+
profileInjected = true;
|
|
721
|
+
profileCountText = cached.profileData?.countText ?? "";
|
|
813
722
|
}
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
723
|
|
|
817
|
-
|
|
818
|
-
|
|
724
|
+
const shouldRecallRes = cached.recallResult;
|
|
725
|
+
|
|
726
|
+
if (!shouldRecallRes.should_recall) {
|
|
727
|
+
const partsToInject: string[] = [];
|
|
728
|
+
if (profileBlock) partsToInject.push(profileBlock);
|
|
729
|
+
if (partsToInject.length > 0) {
|
|
730
|
+
const injectText = partsToInject.join("\n\n");
|
|
731
|
+
const contextPart: Part = {
|
|
732
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
733
|
+
sessionID: input.sessionID,
|
|
734
|
+
messageID: output.message.id,
|
|
735
|
+
type: "text",
|
|
736
|
+
text: injectText,
|
|
737
|
+
synthetic: true,
|
|
738
|
+
};
|
|
739
|
+
output.parts.unshift(contextPart);
|
|
740
|
+
logDebug("memoryInjectionHook profile injected from cache (no-recall)", { sessionId: input.sessionID });
|
|
741
|
+
}
|
|
742
|
+
injectedSessions.add(input.sessionID);
|
|
743
|
+
} else {
|
|
744
|
+
const results = shouldRecallRes.memories ?? [];
|
|
745
|
+
const clustered = shouldRecallRes.clustered;
|
|
746
|
+
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
|
|
747
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
748
|
+
logDebug("memoryInjectionHook dedup (cached)", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
749
|
+
|
|
750
|
+
if (newResults.length === 0) {
|
|
751
|
+
const partsToInject: string[] = [];
|
|
752
|
+
if (profileBlock) partsToInject.push(profileBlock);
|
|
753
|
+
if (partsToInject.length > 0) {
|
|
754
|
+
const injectText = partsToInject.join("\n\n");
|
|
755
|
+
const contextPart: Part = {
|
|
756
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
757
|
+
sessionID: input.sessionID,
|
|
758
|
+
messageID: output.message.id,
|
|
759
|
+
type: "text",
|
|
760
|
+
text: injectText,
|
|
761
|
+
synthetic: true,
|
|
762
|
+
};
|
|
763
|
+
output.parts.unshift(contextPart);
|
|
764
|
+
logDebug("memoryInjectionHook profile injected from cache (dedup)", { sessionId: input.sessionID });
|
|
765
|
+
}
|
|
766
|
+
injectedSessions.add(input.sessionID);
|
|
767
|
+
} else {
|
|
768
|
+
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
769
|
+
const budgetRemaining = maxContentChars - profileChars;
|
|
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
|
+
|
|
777
|
+
const block = clustered
|
|
778
|
+
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
779
|
+
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
780
|
+
|
|
781
|
+
const partsToInject: string[] = [];
|
|
782
|
+
if (block) partsToInject.push(block);
|
|
783
|
+
if (block) partsToInject.push(FETCH_POLICY);
|
|
784
|
+
if (profileBlock) partsToInject.push(profileBlock);
|
|
785
|
+
if (isSaveKeyword) partsToInject.push(KEYWORD_NUDGE);
|
|
786
|
+
|
|
787
|
+
if (partsToInject.length > 0) {
|
|
788
|
+
const injectText = partsToInject.join("\n\n");
|
|
789
|
+
const contextPart: Part = {
|
|
790
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
791
|
+
sessionID: input.sessionID,
|
|
792
|
+
messageID: output.message.id,
|
|
793
|
+
type: "text",
|
|
794
|
+
text: injectText,
|
|
795
|
+
synthetic: true,
|
|
796
|
+
};
|
|
797
|
+
output.parts.unshift(contextPart);
|
|
798
|
+
logDebug("memoryInjectionHook block injected from cache", {
|
|
799
|
+
sessionId: input.sessionID,
|
|
800
|
+
injectTextLen: injectText.length,
|
|
801
|
+
blockPreview: block?.slice(0, 200),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
819
804
|
|
|
820
|
-
|
|
821
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
822
|
-
logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
805
|
+
injectedSessions.add(input.sessionID);
|
|
823
806
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (profileBlock) partsToInject.push(profileBlock);
|
|
828
|
-
if (partsToInject.length > 0) {
|
|
829
|
-
const injectText = partsToInject.join("\n\n");
|
|
830
|
-
const contextPart: Part = {
|
|
831
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
832
|
-
sessionID: input.sessionID,
|
|
833
|
-
messageID: output.message.id,
|
|
834
|
-
type: "text",
|
|
835
|
-
text: injectText,
|
|
836
|
-
synthetic: true,
|
|
837
|
-
};
|
|
838
|
-
output.parts.unshift(contextPart);
|
|
839
|
-
logDebug("memoryInjectionHook profile injected (dedup path)", { sessionId: input.sessionID });
|
|
840
|
-
}
|
|
841
|
-
injectedSessions.add(input.sessionID);
|
|
842
|
-
if (profileInjected && profileIsFirstInjection) {
|
|
843
|
-
showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
|
|
844
|
-
}
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
807
|
+
if (isSaveKeyword) {
|
|
808
|
+
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
809
|
+
}
|
|
847
810
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
811
|
+
const newIds = newResults.map((r) => r.memory.id);
|
|
812
|
+
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
813
|
+
|
|
814
|
+
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
815
|
+
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
816
|
+
const memOther = newResults.length - memDynamic - memStatic;
|
|
817
|
+
|
|
818
|
+
let memCountMsg = "";
|
|
819
|
+
if (memDynamic > 0) memCountMsg += `Dynamic(${memDynamic}) `;
|
|
820
|
+
if (memStatic > 0) memCountMsg += `Static(${memStatic}) `;
|
|
821
|
+
if (memOther > 0) memCountMsg += `Other(${memOther}) `;
|
|
822
|
+
|
|
823
|
+
const categories = categorize(newResults);
|
|
824
|
+
const catSummary = Array.from(categories.entries())
|
|
825
|
+
.map(([label, items]) => `${label}(${items.length})`)
|
|
826
|
+
.join(" · ");
|
|
827
|
+
|
|
828
|
+
let toastTitle: string;
|
|
829
|
+
let toastMessage: string;
|
|
830
|
+
|
|
831
|
+
if (clustered) {
|
|
832
|
+
const clusterCount = clustered.cluster_summaries.length;
|
|
833
|
+
const standaloneCount = clustered.standalone_memories.length;
|
|
834
|
+
toastTitle = `🧠 Context Injected · ${clusterCount} 主题簇${standaloneCount > 0 ? ` · ${standaloneCount} 补充` : ""}`;
|
|
835
|
+
toastMessage = profileInjected
|
|
836
|
+
? `Profile: ${profileCountText} · 聚合记忆展示`
|
|
837
|
+
: `聚合记忆展示`;
|
|
838
|
+
} else {
|
|
839
|
+
toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
|
|
840
|
+
toastMessage = profileInjected
|
|
841
|
+
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
842
|
+
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
843
|
+
}
|
|
864
844
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
845
|
+
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
868
848
|
|
|
869
|
-
|
|
870
|
-
const partsToInject: string[] = [];
|
|
871
|
-
if (profileBlock) partsToInject.push(profileBlock);
|
|
872
|
-
if (block) partsToInject.push(block);
|
|
873
|
-
if (block) partsToInject.push(FETCH_POLICY);
|
|
874
|
-
if (isKeywordTriggered) partsToInject.push(KEYWORD_NUDGE);
|
|
875
|
-
|
|
876
|
-
if (partsToInject.length > 0) {
|
|
877
|
-
const injectText = partsToInject.join("\n\n");
|
|
878
|
-
const contextPart: Part = {
|
|
879
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
880
|
-
sessionID: input.sessionID,
|
|
881
|
-
messageID: output.message.id,
|
|
882
|
-
type: "text",
|
|
883
|
-
text: injectText,
|
|
884
|
-
synthetic: true,
|
|
885
|
-
};
|
|
886
|
-
output.parts.unshift(contextPart);
|
|
887
|
-
logDebug("memoryInjectionHook block injected to output.parts", {
|
|
888
|
-
sessionId: input.sessionID,
|
|
889
|
-
injectTextLen: injectText.length,
|
|
890
|
-
blockPreview: block?.slice(0, 200),
|
|
891
|
-
});
|
|
849
|
+
logDebug("memoryInjectionHook cache hit, injection complete", { sessionId: input.sessionID });
|
|
892
850
|
} else {
|
|
893
|
-
logDebug("memoryInjectionHook
|
|
851
|
+
logDebug("memoryInjectionHook cache miss, first message in session", { sessionId: input.sessionID });
|
|
894
852
|
}
|
|
895
853
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
854
|
+
// ========== Phase B: fire-and-forget async fetch for NEXT round ==========
|
|
855
|
+
const bgSessionId = input.sessionID;
|
|
856
|
+
const bgQueryText = query_text;
|
|
857
|
+
const bgLastQueryText = last_query_text;
|
|
858
|
+
const bgConversationContext = conversationContext;
|
|
859
|
+
const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
|
|
860
|
+
const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
|
|
861
|
+
|
|
862
|
+
Promise.allSettled([
|
|
863
|
+
client.getProfile(),
|
|
864
|
+
client.shouldRecall(
|
|
865
|
+
bgQueryText, bgLastQueryText, bgSessionId,
|
|
866
|
+
similarityThreshold, maxRecallResults,
|
|
867
|
+
bgProjectTags,
|
|
868
|
+
bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined,
|
|
869
|
+
{
|
|
870
|
+
fetch_multiplier: fetchMultiplier,
|
|
871
|
+
topk_cap_multiplier: topkCapMultiplier,
|
|
872
|
+
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
873
|
+
mmr_penalty_factor: mmrPenaltyFactor,
|
|
874
|
+
phase2_multiplier: phase2Multiplier,
|
|
875
|
+
llm_max_eval: llmMaxEval,
|
|
876
|
+
refine_strategy: refineStrategy,
|
|
877
|
+
refine_medium_chars: refineMediumChars,
|
|
878
|
+
},
|
|
879
|
+
bgDirectory,
|
|
880
|
+
),
|
|
881
|
+
])
|
|
882
|
+
.then(([profileRes, recallRes]) => {
|
|
883
|
+
if (recallRes.status === 'rejected') {
|
|
884
|
+
logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
|
|
888
|
+
const shouldRecallRes = recallRes.value;
|
|
889
|
+
if (!shouldRecallRes) {
|
|
890
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
logDebug("memoryInjectionHook background fetch complete", {
|
|
894
|
+
sessionId: bgSessionId,
|
|
895
|
+
shouldRecall: shouldRecallRes.should_recall,
|
|
896
|
+
confidence: shouldRecallRes.confidence,
|
|
897
|
+
memCount: shouldRecallRes.memories?.length ?? 0,
|
|
898
|
+
});
|
|
907
899
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
900
|
+
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
901
|
+
logErr("memoryInjectionHook shouldRecall returned incomplete data", {
|
|
902
|
+
shouldRecall: shouldRecallRes.should_recall,
|
|
903
|
+
hasMemories: !!shouldRecallRes.memories,
|
|
904
|
+
});
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
911
907
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
908
|
+
let bgProfileBlock = "";
|
|
909
|
+
let bgProfileCountText = "";
|
|
910
|
+
let bgProfileInjected = false;
|
|
911
|
+
|
|
912
|
+
if (profile) {
|
|
913
|
+
const lastInjected = profileInjectedSessions.get(bgSessionId);
|
|
914
|
+
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
|
|
915
|
+
if (ttlExpired) {
|
|
916
|
+
const built = buildProfileBlock(profile);
|
|
917
|
+
if (built) {
|
|
918
|
+
bgProfileBlock = built.block;
|
|
919
|
+
bgProfileCountText = built.countText;
|
|
920
|
+
bgProfileInjected = true;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
916
924
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
925
|
+
recallCache.set(bgSessionId, {
|
|
926
|
+
profileBlock: bgProfileBlock,
|
|
927
|
+
recallResult: shouldRecallRes,
|
|
928
|
+
profileData: { countText: bgProfileCountText },
|
|
929
|
+
timestamp: Date.now(),
|
|
930
|
+
});
|
|
921
931
|
|
|
922
|
-
|
|
923
|
-
|
|
932
|
+
if (recallCache.size > 50) {
|
|
933
|
+
let oldestKey: string | null = null;
|
|
934
|
+
let oldestTime = Infinity;
|
|
935
|
+
for (const [k, v] of recallCache) {
|
|
936
|
+
if (v.timestamp < oldestTime) {
|
|
937
|
+
oldestTime = v.timestamp;
|
|
938
|
+
oldestKey = k;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (oldestKey) recallCache.delete(oldestKey);
|
|
942
|
+
}
|
|
924
943
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
|
|
934
|
-
toastMessage = profileInjected
|
|
935
|
-
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
936
|
-
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
937
|
-
}
|
|
944
|
+
if (shouldRecallRes.should_recall) {
|
|
945
|
+
const results = shouldRecallRes.memories ?? [];
|
|
946
|
+
const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set<string>();
|
|
947
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
948
|
+
if (newResults.length > 0) {
|
|
949
|
+
const newIds = newResults.map((r) => r.memory.id);
|
|
950
|
+
injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
|
|
951
|
+
}
|
|
938
952
|
|
|
939
|
-
|
|
953
|
+
const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
|
|
954
|
+
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
955
|
+
const maxScore = storedMemoryIds.length > 0
|
|
956
|
+
? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
|
|
957
|
+
: 0;
|
|
958
|
+
|
|
959
|
+
const bgBlock = shouldRecallRes.clustered
|
|
960
|
+
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
961
|
+
: buildContextBlock(newResults, maxContentLength);
|
|
962
|
+
const bgInjectedContent = bgBlock ?? undefined;
|
|
963
|
+
|
|
964
|
+
const items = [
|
|
965
|
+
...(shouldRecallRes.memories?.map((r) => ({
|
|
966
|
+
memory_id: r.memory.id,
|
|
967
|
+
score: r.score,
|
|
968
|
+
refine_relevance: r.refine_relevance,
|
|
969
|
+
refine_reasoning: r.refine_reasoning,
|
|
970
|
+
is_kept: true,
|
|
971
|
+
})) ?? []),
|
|
972
|
+
...(shouldRecallRes.discarded?.map((d) => ({
|
|
973
|
+
memory_id: d.memory_id,
|
|
974
|
+
score: d.score,
|
|
975
|
+
refine_relevance: d.refine_relevance,
|
|
976
|
+
refine_reasoning: d.refine_reasoning,
|
|
977
|
+
is_kept: false,
|
|
978
|
+
})) ?? []),
|
|
979
|
+
];
|
|
980
|
+
|
|
981
|
+
client.createRecallEvent({
|
|
982
|
+
session_id: bgSessionId,
|
|
983
|
+
recall_type: "auto",
|
|
984
|
+
query_text: bgQueryText,
|
|
985
|
+
max_score: maxScore,
|
|
986
|
+
llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
987
|
+
profile_injected: bgProfileInjected,
|
|
988
|
+
kept_count: storedMemoryIds.length,
|
|
989
|
+
discarded_count: storedDiscardedIds.length,
|
|
990
|
+
injected_count: newResults.length,
|
|
991
|
+
profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
|
|
992
|
+
injected_content: bgInjectedContent,
|
|
993
|
+
items: items.length > 0 ? items : undefined,
|
|
994
|
+
}).catch((e: unknown) => {
|
|
995
|
+
logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
.catch((err: unknown) => {
|
|
1000
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1001
|
+
logErr("memoryInjectionHook background fetch failed", { error: errMsg });
|
|
1002
|
+
if (errMsg.includes("[cerebro]")) {
|
|
1003
|
+
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
1004
|
+
if (cleanMsg.startsWith("500")) {
|
|
1005
|
+
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
1006
|
+
} else if (cleanMsg.includes("timed out")) {
|
|
1007
|
+
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
1008
|
+
}
|
|
1009
|
+
} else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
1010
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
940
1013
|
} catch (err) {
|
|
941
1014
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
942
1015
|
if (errMsg.includes("[cerebro]")) {
|
|
@@ -974,8 +1047,8 @@ export function keywordDetectionHook(_client: CerebroClient, _containerTags: str
|
|
|
974
1047
|
firstMessages.set(input.sessionID, textContent);
|
|
975
1048
|
}
|
|
976
1049
|
|
|
977
|
-
if (
|
|
978
|
-
|
|
1050
|
+
if (detectSaveKeyword(textContent)) {
|
|
1051
|
+
saveKeywordDetectedSessions.add(input.sessionID);
|
|
979
1052
|
logDebug("keywordDetectionHook triggered", { sessionId: input.sessionID });
|
|
980
1053
|
}
|
|
981
1054
|
|
|
@@ -1100,6 +1173,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1100
1173
|
if (input.sessionID) {
|
|
1101
1174
|
sessionMessages.delete(input.sessionID);
|
|
1102
1175
|
profileInjectedSessions.delete(input.sessionID);
|
|
1176
|
+
recallCache.delete(input.sessionID);
|
|
1103
1177
|
firstMessages.delete(input.sessionID);
|
|
1104
1178
|
}
|
|
1105
1179
|
return;
|
|
@@ -1131,6 +1205,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1131
1205
|
if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
|
|
1132
1206
|
sessionMessages.delete(input.sessionID);
|
|
1133
1207
|
profileInjectedSessions.delete(input.sessionID);
|
|
1208
|
+
recallCache.delete(input.sessionID);
|
|
1134
1209
|
firstMessages.delete(input.sessionID);
|
|
1135
1210
|
} else {
|
|
1136
1211
|
const messages = sessionMessages.get(input.sessionID)!;
|
|
@@ -1159,7 +1234,9 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1159
1234
|
}
|
|
1160
1235
|
// Cleanup tracked messages regardless of ingest result
|
|
1161
1236
|
sessionMessages.delete(input.sessionID);
|
|
1237
|
+
injectedSessions.delete(input.sessionID);
|
|
1162
1238
|
profileInjectedSessions.delete(input.sessionID);
|
|
1239
|
+
recallCache.delete(input.sessionID);
|
|
1163
1240
|
firstMessages.delete(input.sessionID);
|
|
1164
1241
|
if (input.sessionID) {
|
|
1165
1242
|
const deleted = pendingToolCalls.delete(input.sessionID);
|