@mingxy/cerebro 1.14.6 → 1.15.2
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/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks.d.ts +10 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +395 -19
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +3 -0
- package/src/hooks.ts +434 -17
- package/src/index.ts +29 -10
package/src/hooks.ts
CHANGED
|
@@ -130,7 +130,7 @@ function showToast(tui: any, title: string, message: string, variant: string = "
|
|
|
130
130
|
try {
|
|
131
131
|
tui.showToast({ body: { title, message, variant, duration: 5000 } });
|
|
132
132
|
} catch (err) {
|
|
133
|
-
|
|
133
|
+
logErr("showToast failed", { error: String(err) });
|
|
134
134
|
}
|
|
135
135
|
}, delayMs);
|
|
136
136
|
}
|
|
@@ -172,6 +172,18 @@ 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 }>>();
|
|
174
174
|
const profileInjectedSessions = new Map<string, number>();
|
|
175
|
+
const injectedSessions = new Set<string>();
|
|
176
|
+
const compactingSummaryCooldown = new Map<string, number>();
|
|
177
|
+
|
|
178
|
+
function hashString(str: string): string {
|
|
179
|
+
let hash = 0;
|
|
180
|
+
for (let i = 0; i < str.length; i++) {
|
|
181
|
+
const char = str.charCodeAt(i);
|
|
182
|
+
hash = ((hash << 5) - hash) + char;
|
|
183
|
+
hash |= 0;
|
|
184
|
+
}
|
|
185
|
+
return hash.toString(36);
|
|
186
|
+
}
|
|
175
187
|
|
|
176
188
|
function formatRelativeAge(isoDate: string): string {
|
|
177
189
|
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
@@ -236,7 +248,9 @@ function formatMemoryLine(r: SearchResult, maxContentLength: number): string {
|
|
|
236
248
|
|
|
237
249
|
const FETCH_POLICY = [
|
|
238
250
|
"<cerebro-fetch-policy>",
|
|
239
|
-
"Each memory above is a condensed summary
|
|
251
|
+
"IMPORTANT: Each memory above is a condensed summary. The full version contains critical details that may change your response quality.",
|
|
252
|
+
"You MUST use memory_get(\"id\") to retrieve the complete content, or memory_search(\"query\") to find specific memories before making decisions based on any summary.",
|
|
253
|
+
"Do NOT rely on condensed summaries alone — depth of recall determines quality of response.",
|
|
240
254
|
"</cerebro-fetch-policy>",
|
|
241
255
|
].join("\n");
|
|
242
256
|
|
|
@@ -601,6 +615,348 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
601
615
|
};
|
|
602
616
|
}
|
|
603
617
|
|
|
618
|
+
export function memoryInjectionHook(
|
|
619
|
+
client: CerebroClient,
|
|
620
|
+
containerTags: string[],
|
|
621
|
+
tui: any,
|
|
622
|
+
config: Partial<OmemPluginConfig> = {},
|
|
623
|
+
getAgentName?: () => string,
|
|
624
|
+
directory?: string,
|
|
625
|
+
) {
|
|
626
|
+
const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
|
|
627
|
+
const maxRecallResults = config.recall?.maxRecallResults ?? 10;
|
|
628
|
+
const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
|
|
629
|
+
const topkCapMultiplier = config.recall?.topkCapMultiplier ?? 2;
|
|
630
|
+
const mmrJaccardThreshold = config.recall?.mmrJaccardThreshold ?? 0.85;
|
|
631
|
+
const mmrPenaltyFactor = config.recall?.mmrPenaltyFactor ?? 0.5;
|
|
632
|
+
const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
|
|
633
|
+
const llmMaxEval = config.recall?.llmMaxEval ?? 15;
|
|
634
|
+
const refineStrategy = config.recall?.refineStrategy ?? "balanced";
|
|
635
|
+
const refineMediumChars = config.recall?.refineMediumChars ?? 200;
|
|
636
|
+
const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
|
|
637
|
+
const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
|
|
638
|
+
const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
|
|
639
|
+
|
|
640
|
+
return async (
|
|
641
|
+
input: { sessionID?: string; messageID?: string; model: Model },
|
|
642
|
+
output: { message: UserMessage; parts: Part[] },
|
|
643
|
+
) => {
|
|
644
|
+
if (!input.sessionID) return;
|
|
645
|
+
|
|
646
|
+
const agentId = getAgentName?.() || process.env.OMEM_AGENT_ID || "opencode";
|
|
647
|
+
const policy = resolveAgentPolicy(agentId, config);
|
|
648
|
+
if (policy === "none") return;
|
|
649
|
+
|
|
650
|
+
const isFirstInjection = !injectedSessions.has(input.sessionID);
|
|
651
|
+
const isKeywordTriggered = keywordDetectedSessions.has(input.sessionID);
|
|
652
|
+
if (!isFirstInjection && !isKeywordTriggered) return;
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy, isFirstInjection, isKeywordTriggered, similarityThreshold, maxRecallResults });
|
|
656
|
+
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
657
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
658
|
+
|
|
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
|
+
if (userMessages.length === 0) {
|
|
697
|
+
logDebug("memoryInjectionHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
702
|
+
const query_text = extractUserRequest(rawQuery);
|
|
703
|
+
if (!query_text) {
|
|
704
|
+
logDebug("memoryInjectionHook filtered system injection (profile already injected above)", { rawQueryPrefix: rawQuery.slice(0, 60) });
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
708
|
+
|
|
709
|
+
const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
|
|
710
|
+
|
|
711
|
+
const conversationContext = userMessages.length >= 2
|
|
712
|
+
? userMessages.slice(-4, -1).map((m) => {
|
|
713
|
+
const stripped = stripPrivateContent(m.content);
|
|
714
|
+
return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
|
|
715
|
+
})
|
|
716
|
+
: undefined;
|
|
717
|
+
|
|
718
|
+
const shouldRecallRes = await client.shouldRecall(
|
|
719
|
+
query_text, last_query_text, input.sessionID,
|
|
720
|
+
similarityThreshold, maxRecallResults,
|
|
721
|
+
projectTags.length > 0 ? projectTags : undefined,
|
|
722
|
+
conversationContext && conversationContext.length > 0 ? conversationContext : undefined,
|
|
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
|
+
};
|
|
791
|
+
|
|
792
|
+
// --- no-recall path: inject profile only ---
|
|
793
|
+
if (!shouldRecallRes.should_recall) {
|
|
794
|
+
const partsToInject: string[] = [];
|
|
795
|
+
if (profileBlock) partsToInject.push(profileBlock);
|
|
796
|
+
if (partsToInject.length > 0) {
|
|
797
|
+
const injectText = partsToInject.join("\n\n");
|
|
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);
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const results = shouldRecallRes.memories ?? [];
|
|
818
|
+
const clustered = shouldRecallRes.clustered;
|
|
819
|
+
|
|
820
|
+
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
|
|
821
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
822
|
+
logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
823
|
+
|
|
824
|
+
// --- dedup path: inject profile only ---
|
|
825
|
+
if (newResults.length === 0) {
|
|
826
|
+
const partsToInject: string[] = [];
|
|
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
|
+
}
|
|
847
|
+
|
|
848
|
+
// --- Token Budget Calculation ---
|
|
849
|
+
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
850
|
+
const budgetRemaining = maxContentChars - profileChars;
|
|
851
|
+
if (budgetRemaining < 0) {
|
|
852
|
+
logDebug("memoryInjectionHook budget overflow", { profileChars, maxContentChars, deficit: -budgetRemaining });
|
|
853
|
+
}
|
|
854
|
+
const itemCount = clustered
|
|
855
|
+
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
856
|
+
: newResults.length;
|
|
857
|
+
const dynamicMaxContentLength = itemCount > 0
|
|
858
|
+
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
859
|
+
: maxContentLength;
|
|
860
|
+
logDebug("memoryInjectionHook budget", {
|
|
861
|
+
maxContentChars, profileChars, budgetRemaining, itemCount,
|
|
862
|
+
configuredMax: maxContentLength, dynamicMax: dynamicMaxContentLength,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const block = clustered
|
|
866
|
+
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
867
|
+
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
868
|
+
|
|
869
|
+
// ★★★ Core change: inject via output.parts.unshift + synthetic:true ★★★
|
|
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
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
logDebug("memoryInjectionHook no content to inject", { sessionId: input.sessionID });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
injectedSessions.add(input.sessionID);
|
|
897
|
+
|
|
898
|
+
if (isKeywordTriggered) {
|
|
899
|
+
keywordDetectedSessions.delete(input.sessionID);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const newIds = newResults.map((r) => r.memory.id);
|
|
903
|
+
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
904
|
+
logDebug("memoryInjectionHook injection complete", { newIds: newIds.length, clustered: !!clustered, sessionId: input.sessionID });
|
|
905
|
+
|
|
906
|
+
await createEventAndReturn(newResults.length, storedMemoryIds.length, storedDiscardedIds.length, block || undefined);
|
|
907
|
+
|
|
908
|
+
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
909
|
+
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
910
|
+
const memOther = newResults.length - memDynamic - memStatic;
|
|
911
|
+
|
|
912
|
+
let memCountMsg = "";
|
|
913
|
+
if (memDynamic > 0) memCountMsg += `Dynamic(${memDynamic}) `;
|
|
914
|
+
if (memStatic > 0) memCountMsg += `Static(${memStatic}) `;
|
|
915
|
+
if (memOther > 0) memCountMsg += `Other(${memOther}) `;
|
|
916
|
+
|
|
917
|
+
const categories = categorize(newResults);
|
|
918
|
+
const catSummary = Array.from(categories.entries())
|
|
919
|
+
.map(([label, items]) => `${label}(${items.length})`)
|
|
920
|
+
.join(" · ");
|
|
921
|
+
|
|
922
|
+
let toastTitle: string;
|
|
923
|
+
let toastMessage: string;
|
|
924
|
+
|
|
925
|
+
if (clustered) {
|
|
926
|
+
const clusterCount = clustered.cluster_summaries.length;
|
|
927
|
+
const standaloneCount = clustered.standalone_memories.length;
|
|
928
|
+
toastTitle = `🧠 Context Injected · ${clusterCount} 主题簇${standaloneCount > 0 ? ` · ${standaloneCount} 补充` : ""}`;
|
|
929
|
+
toastMessage = profileInjected
|
|
930
|
+
? `Profile: ${profileCountText} · 聚合记忆展示`
|
|
931
|
+
: `聚合记忆展示`;
|
|
932
|
+
} else {
|
|
933
|
+
toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
|
|
934
|
+
toastMessage = profileInjected
|
|
935
|
+
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
936
|
+
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
942
|
+
if (errMsg.includes("[cerebro]")) {
|
|
943
|
+
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
944
|
+
if (cleanMsg.startsWith("500")) {
|
|
945
|
+
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
946
|
+
} else if (cleanMsg.includes("timed out")) {
|
|
947
|
+
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
948
|
+
} else {
|
|
949
|
+
showToast(tui, "🧠 Cerebro Error", cleanMsg.substring(0, 150), "error");
|
|
950
|
+
}
|
|
951
|
+
} else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
952
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
953
|
+
} else {
|
|
954
|
+
showToast(tui, "🧠 Memory Recall Error", errMsg.substring(0, 100), "error");
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
604
960
|
export function keywordDetectionHook(_client: CerebroClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart", config: Partial<OmemPluginConfig> = {}, agentId?: string) {
|
|
605
961
|
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
606
962
|
return async (
|
|
@@ -643,6 +999,57 @@ export function keywordDetectionHook(_client: CerebroClient, _containerTags: str
|
|
|
643
999
|
};
|
|
644
1000
|
}
|
|
645
1001
|
|
|
1002
|
+
export function createCerebroCompactionPrompt(
|
|
1003
|
+
context: string[],
|
|
1004
|
+
projectMemories: SearchResult[],
|
|
1005
|
+
): string {
|
|
1006
|
+
const sections: string[] = [
|
|
1007
|
+
"[Cerebro Compaction Context]",
|
|
1008
|
+
"",
|
|
1009
|
+
"## 1. User's Original Request",
|
|
1010
|
+
"Preserve the user's verbatim original request from the conversation above.",
|
|
1011
|
+
"",
|
|
1012
|
+
"## 2. Final Goal",
|
|
1013
|
+
"What is the ultimate objective the user wants to achieve?",
|
|
1014
|
+
"",
|
|
1015
|
+
"## 3. Work Completed",
|
|
1016
|
+
"List all completed work with file paths and technical decisions made.",
|
|
1017
|
+
"",
|
|
1018
|
+
"## 4. Remaining Tasks",
|
|
1019
|
+
"What is still unfinished or pending?",
|
|
1020
|
+
"",
|
|
1021
|
+
"## 5. Prohibited Actions",
|
|
1022
|
+
"Key constraints and forbidden operations to remember.",
|
|
1023
|
+
"",
|
|
1024
|
+
"## 6. Existing Project Knowledge",
|
|
1025
|
+
];
|
|
1026
|
+
|
|
1027
|
+
if (projectMemories.length > 0) {
|
|
1028
|
+
const memBlock = projectMemories
|
|
1029
|
+
.slice(0, 10)
|
|
1030
|
+
.map((r) => {
|
|
1031
|
+
const content = r.memory.content ?? "";
|
|
1032
|
+
const truncated = content.length > 200 ? content.slice(0, 200) + "..." : content;
|
|
1033
|
+
return ` - [${r.memory.category ?? "general"}] ${truncated}`;
|
|
1034
|
+
})
|
|
1035
|
+
.join("\n");
|
|
1036
|
+
sections.push(memBlock);
|
|
1037
|
+
} else {
|
|
1038
|
+
sections.push(" (No project memories retrieved)");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (context.length > 0) {
|
|
1042
|
+
sections.push("");
|
|
1043
|
+
sections.push("### Additional Context");
|
|
1044
|
+
sections.push(...context);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
sections.push("");
|
|
1048
|
+
sections.push("IMPORTANT: Output must preserve the user's original language (Chinese/English/etc). Do not translate.");
|
|
1049
|
+
|
|
1050
|
+
return sections.join("\n");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
646
1053
|
export function compactingHook(client: CerebroClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart", isAutoStoreEnabled?: (sessionId: string | undefined) => boolean, getMainSessionId?: () => string | undefined, sdkClient?: any, config: Partial<OmemPluginConfig> = {}, agentId?: string, directory?: string) {
|
|
647
1054
|
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
648
1055
|
return async (
|
|
@@ -654,18 +1061,18 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
654
1061
|
// Search (read) always runs — even readonly agents need context during compacting
|
|
655
1062
|
try {
|
|
656
1063
|
const results = await client.searchMemories("*", 20, undefined, containerTags);
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1064
|
+
const compactionPrompt = createCerebroCompactionPrompt(output.context, results);
|
|
1065
|
+
if (output.prompt !== undefined) {
|
|
1066
|
+
output.prompt = compactionPrompt;
|
|
1067
|
+
} else if (output.context.length > 0) {
|
|
1068
|
+
output.context[output.context.length - 1] += "\n\n" + compactionPrompt;
|
|
1069
|
+
} else {
|
|
1070
|
+
output.context.push(compactionPrompt);
|
|
1071
|
+
}
|
|
1072
|
+
if (output.context.length > 0) {
|
|
1073
|
+
output.context[output.context.length - 1] += "\n\n" + FETCH_POLICY;
|
|
1074
|
+
} else {
|
|
1075
|
+
output.context.push(FETCH_POLICY);
|
|
669
1076
|
}
|
|
670
1077
|
// 将compacting搜索结果的ID写入injectedMemoryIds,避免后续autoRecall重复注入
|
|
671
1078
|
if (input.sessionID && results.length > 0) {
|
|
@@ -870,9 +1277,19 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
870
1277
|
logInfo("compactingHook: storing compact summary", {
|
|
871
1278
|
summaryLen: summaryText.length, msgId: compactMsg.info?.id,
|
|
872
1279
|
});
|
|
1280
|
+
// Dedup check: 30s cooldown per session+content hash
|
|
1281
|
+
const summaryHash = `${pollSessionId}:${hashString(summaryText)}`;
|
|
1282
|
+
const lastCompacting = compactingSummaryCooldown.get(summaryHash);
|
|
1283
|
+
if (lastCompacting && Date.now() - lastCompacting < 30000) {
|
|
1284
|
+
logDebug("compactingHook summary dedup", { sessionId: pollSessionId });
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
compactingSummaryCooldown.set(summaryHash, Date.now());
|
|
1288
|
+
|
|
1289
|
+
const prefixedSummary = `[Session Summary] ${summaryText}`;
|
|
873
1290
|
try {
|
|
874
1291
|
const result = await client.ingestMessages(
|
|
875
|
-
[{ role: "user" as const, content:
|
|
1292
|
+
[{ role: "user" as const, content: prefixedSummary }],
|
|
876
1293
|
{
|
|
877
1294
|
mode: ingestMode,
|
|
878
1295
|
tags: [...containerTags, "auto-capture", "compact-summary"],
|
|
@@ -1064,9 +1481,9 @@ export function buildWhisperText(toolNames: string[], maxToolNames: number): str
|
|
|
1064
1481
|
const lines: string[] = ["<cerebro-memory-activation>"];
|
|
1065
1482
|
|
|
1066
1483
|
if (toolNames.length <= maxToolNames) {
|
|
1067
|
-
lines.push(`
|
|
1484
|
+
lines.push(`Before using ${toolNames.join(", ")}, memory_search() may surface relevant past decisions or patterns. Brief recall → better outcomes.`);
|
|
1068
1485
|
} else {
|
|
1069
|
-
lines.push("Before you act —
|
|
1486
|
+
lines.push("Before you act — memory_search() surfaces cross-session context: past decisions, user preferences, hard-won insights. The strongest responses are built on remembered context.");
|
|
1070
1487
|
}
|
|
1071
1488
|
|
|
1072
1489
|
lines.push("</cerebro-memory-activation>");
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { join, dirname } from "node:path";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { CerebroClient } from "./client.js";
|
|
7
|
-
import { autoRecallHook, autocontinueHook, compactingHook, keywordDetectionHook, sessionIdleHook, soulWhisperToolTracker, pendingToolCalls, buildWhisperText } from "./hooks.js";
|
|
7
|
+
import { autoRecallHook, memoryInjectionHook, autocontinueHook, compactingHook, keywordDetectionHook, sessionIdleHook, soulWhisperToolTracker, pendingToolCalls, buildWhisperText } from "./hooks.js";
|
|
8
8
|
import { getUserTag, getProjectTag } from "./tags.js";
|
|
9
9
|
import { buildTools } from "./tools.js";
|
|
10
10
|
import { logInfo, logDebug, logError } from "./logger.js";
|
|
@@ -55,7 +55,7 @@ function showToast(tui: any, title: string, message?: string, variant: string =
|
|
|
55
55
|
}
|
|
56
56
|
tui.showToast({ body });
|
|
57
57
|
} catch (err) {
|
|
58
|
-
|
|
58
|
+
logError("showToast failed", { error: String(err) });
|
|
59
59
|
}
|
|
60
60
|
}, 3000);
|
|
61
61
|
}
|
|
@@ -119,11 +119,7 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
119
119
|
let mainSessionLocked = false;
|
|
120
120
|
let cachedAgentName: string | undefined;
|
|
121
121
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const wrappedRecallHook = async (input: any, output: any) => {
|
|
125
|
-
await recallHook(input, output);
|
|
126
|
-
|
|
122
|
+
const soulWhisperSystemHook = async (input: any, output: any) => {
|
|
127
123
|
// ── Soul Whisper: inject to system prompt (v2 — system.transform) ──
|
|
128
124
|
if (config.soulWhisper?.enabled !== false) {
|
|
129
125
|
const sid = input.sessionID || "_default";
|
|
@@ -145,6 +141,29 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
145
141
|
}
|
|
146
142
|
};
|
|
147
143
|
|
|
144
|
+
// ── Fallback strategy: "parts" (new) vs "system" (legacy) ──
|
|
145
|
+
const strategy = config.injectionStrategy ?? "parts";
|
|
146
|
+
|
|
147
|
+
const chatMessageHook = strategy === "parts"
|
|
148
|
+
? async (input: any, output: any) => {
|
|
149
|
+
// New path: keyword detection + memory injection
|
|
150
|
+
await keywordDetectionHook(cerebroClient, containerTags, config.ingest.autoCaptureThreshold, tui, config.ingest.ingestMode, config, agentId)(input, output);
|
|
151
|
+
await memoryInjectionHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId, directory)(input, output);
|
|
152
|
+
}
|
|
153
|
+
: async (input: any, output: any) => {
|
|
154
|
+
// Fallback: keyword detection only (memory injection via system.transform legacy autoRecallHook)
|
|
155
|
+
await keywordDetectionHook(cerebroClient, containerTags, config.ingest.autoCaptureThreshold, tui, config.ingest.ingestMode, config, agentId)(input, output);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const systemTransformHook = strategy === "parts"
|
|
159
|
+
? soulWhisperSystemHook
|
|
160
|
+
: async (input: any, output: any) => {
|
|
161
|
+
// Fallback: legacy autoRecallHook + soulWhisper
|
|
162
|
+
const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId, directory);
|
|
163
|
+
await recallHook(input, output);
|
|
164
|
+
await soulWhisperSystemHook(input, output);
|
|
165
|
+
};
|
|
166
|
+
|
|
148
167
|
return {
|
|
149
168
|
config: async (cfg: any) => {
|
|
150
169
|
cfg.command ??= {};
|
|
@@ -154,15 +173,15 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
154
173
|
};
|
|
155
174
|
},
|
|
156
175
|
"experimental.chat.system.transform": async (input: any, output: any) => {
|
|
157
|
-
logDebug("transform input", { sessionID: input.sessionID });
|
|
176
|
+
logDebug("transform input", { sessionID: input.sessionID, strategy });
|
|
158
177
|
if (input.sessionID && !mainSessionLocked) {
|
|
159
178
|
mainSessionId = input.sessionID;
|
|
160
179
|
mainSessionLocked = true;
|
|
161
180
|
logInfo("mainSessionId locked", { sessionId: input.sessionID });
|
|
162
181
|
}
|
|
163
|
-
return
|
|
182
|
+
return systemTransformHook(input, output);
|
|
164
183
|
},
|
|
165
|
-
"chat.message":
|
|
184
|
+
"chat.message": chatMessageHook,
|
|
166
185
|
"experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
|
|
167
186
|
"experimental.compaction.autocontinue": autocontinueHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
|
|
168
187
|
tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId, getProjectPath: () => directory }),
|