@mingxy/cerebro 1.15.5 → 1.15.7
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/hooks.d.ts.map +1 -1
- package/dist/hooks.js +338 -241
- package/dist/hooks.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/package.json +1 -1
- package/src/hooks.ts +214 -108
package/src/hooks.ts
CHANGED
|
@@ -707,23 +707,117 @@ export function memoryInjectionHook(
|
|
|
707
707
|
})
|
|
708
708
|
: undefined;
|
|
709
709
|
|
|
710
|
-
// ========== Phase A:
|
|
711
|
-
|
|
710
|
+
// ========== Phase A: unified data fetch + injection ==========
|
|
711
|
+
let shouldRecallRes: ShouldRecallResponse;
|
|
712
712
|
let profileBlock = "";
|
|
713
713
|
let profileInjected = false;
|
|
714
714
|
let profileCountText = "";
|
|
715
|
+
let isCacheHit = false;
|
|
716
|
+
|
|
717
|
+
const cached = recallCache.get(input.sessionID);
|
|
715
718
|
|
|
716
719
|
if (cached) {
|
|
717
|
-
|
|
720
|
+
isCacheHit = true;
|
|
721
|
+
shouldRecallRes = cached.recallResult;
|
|
718
722
|
if (cached.profileBlock) {
|
|
719
723
|
profileBlock = cached.profileBlock;
|
|
720
724
|
profileInjected = true;
|
|
721
725
|
profileCountText = cached.profileData?.countText ?? "";
|
|
722
726
|
}
|
|
727
|
+
} else {
|
|
728
|
+
// cache miss: synchronous await (first message takes 5-8s, but gets injection)
|
|
729
|
+
const [profile, recallRes] = await Promise.all([
|
|
730
|
+
client.getProfile(),
|
|
731
|
+
client.shouldRecall(
|
|
732
|
+
query_text, last_query_text, input.sessionID,
|
|
733
|
+
similarityThreshold, maxRecallResults,
|
|
734
|
+
projectTags.length > 0 ? projectTags : undefined,
|
|
735
|
+
conversationContext && conversationContext.length > 0 ? conversationContext : undefined,
|
|
736
|
+
{
|
|
737
|
+
fetch_multiplier: fetchMultiplier,
|
|
738
|
+
topk_cap_multiplier: topkCapMultiplier,
|
|
739
|
+
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
740
|
+
mmr_penalty_factor: mmrPenaltyFactor,
|
|
741
|
+
phase2_multiplier: phase2Multiplier,
|
|
742
|
+
llm_max_eval: llmMaxEval,
|
|
743
|
+
refine_strategy: refineStrategy,
|
|
744
|
+
refine_medium_chars: refineMediumChars,
|
|
745
|
+
},
|
|
746
|
+
directory || process.env.OMEM_PROJECT_DIR,
|
|
747
|
+
),
|
|
748
|
+
]);
|
|
749
|
+
if (!recallRes) {
|
|
750
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API", "error", toastDelayMs);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
shouldRecallRes = recallRes;
|
|
754
|
+
|
|
755
|
+
// build profile block
|
|
756
|
+
if (profile) {
|
|
757
|
+
const built = buildProfileBlock(profile);
|
|
758
|
+
if (built) {
|
|
759
|
+
profileBlock = built.block;
|
|
760
|
+
profileCountText = built.countText;
|
|
761
|
+
profileInjected = true;
|
|
762
|
+
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
763
|
+
}
|
|
764
|
+
}
|
|
723
765
|
|
|
724
|
-
|
|
766
|
+
// write cache for next round
|
|
767
|
+
recallCache.set(input.sessionID, {
|
|
768
|
+
profileBlock,
|
|
769
|
+
recallResult: shouldRecallRes,
|
|
770
|
+
profileData: { countText: profileCountText },
|
|
771
|
+
timestamp: Date.now(),
|
|
772
|
+
});
|
|
725
773
|
|
|
726
|
-
|
|
774
|
+
// LRU eviction
|
|
775
|
+
if (recallCache.size > 50) {
|
|
776
|
+
let oldestKey: string | null = null;
|
|
777
|
+
let oldestTime = Infinity;
|
|
778
|
+
for (const [k, v] of recallCache) {
|
|
779
|
+
if (v.timestamp < oldestTime) { oldestTime = v.timestamp; oldestKey = k; }
|
|
780
|
+
}
|
|
781
|
+
if (oldestKey) recallCache.delete(oldestKey);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// defensive check
|
|
785
|
+
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
786
|
+
logErr("memoryInjectionHook shouldRecall returned incomplete data", { shouldRecall: shouldRecallRes.should_recall, hasMemories: !!shouldRecallRes.memories });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
logDebug("memoryInjectionHook cache miss, fetched synchronously", { sessionId: input.sessionID, shouldRecall: shouldRecallRes.should_recall, memCount: shouldRecallRes.memories?.length ?? 0 });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ========== unified injection logic (cache hit + cache miss share this) ==========
|
|
794
|
+
if (!shouldRecallRes.should_recall) {
|
|
795
|
+
// no-recall path: inject profile only
|
|
796
|
+
const partsToInject: string[] = [];
|
|
797
|
+
if (profileBlock) partsToInject.push(profileBlock);
|
|
798
|
+
if (partsToInject.length > 0) {
|
|
799
|
+
const injectText = partsToInject.join("\n\n");
|
|
800
|
+
const contextPart: Part = {
|
|
801
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
802
|
+
sessionID: input.sessionID,
|
|
803
|
+
messageID: output.message.id,
|
|
804
|
+
type: "text",
|
|
805
|
+
text: injectText,
|
|
806
|
+
synthetic: true,
|
|
807
|
+
};
|
|
808
|
+
output.parts.unshift(contextPart);
|
|
809
|
+
logDebug("memoryInjectionHook profile injected (no-recall)", { sessionId: input.sessionID });
|
|
810
|
+
}
|
|
811
|
+
injectedSessions.add(input.sessionID);
|
|
812
|
+
showToast(tui, "🧠 Profile Injected", profileCountText ? `Profile: ${profileCountText} · no recall needed` : "No memory recall needed", "success", toastDelayMs);
|
|
813
|
+
} else {
|
|
814
|
+
const results = shouldRecallRes.memories ?? [];
|
|
815
|
+
const clustered = shouldRecallRes.clustered;
|
|
816
|
+
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
|
|
817
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
818
|
+
logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
819
|
+
|
|
820
|
+
if (newResults.length === 0) {
|
|
727
821
|
const partsToInject: string[] = [];
|
|
728
822
|
if (profileBlock) partsToInject.push(profileBlock);
|
|
729
823
|
if (partsToInject.length > 0) {
|
|
@@ -737,121 +831,132 @@ export function memoryInjectionHook(
|
|
|
737
831
|
synthetic: true,
|
|
738
832
|
};
|
|
739
833
|
output.parts.unshift(contextPart);
|
|
740
|
-
logDebug("memoryInjectionHook profile injected
|
|
834
|
+
logDebug("memoryInjectionHook profile injected (dedup)", { sessionId: input.sessionID });
|
|
741
835
|
}
|
|
742
836
|
injectedSessions.add(input.sessionID);
|
|
743
837
|
} else {
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
}
|
|
838
|
+
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
839
|
+
const budgetRemaining = maxContentChars - profileChars;
|
|
840
|
+
const itemCount = clustered
|
|
841
|
+
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
842
|
+
: newResults.length;
|
|
843
|
+
const dynamicMaxContentLength = itemCount > 0
|
|
844
|
+
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
845
|
+
: maxContentLength;
|
|
846
|
+
|
|
847
|
+
const block = clustered
|
|
848
|
+
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
849
|
+
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
804
850
|
|
|
805
|
-
|
|
851
|
+
const partsToInject: string[] = [];
|
|
852
|
+
if (block) partsToInject.push(block);
|
|
853
|
+
if (block) partsToInject.push(FETCH_POLICY);
|
|
854
|
+
if (profileBlock) partsToInject.push(profileBlock);
|
|
855
|
+
if (isSaveKeyword) partsToInject.push(KEYWORD_NUDGE);
|
|
806
856
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
857
|
+
if (partsToInject.length > 0) {
|
|
858
|
+
const injectText = partsToInject.join("\n\n");
|
|
859
|
+
const contextPart: Part = {
|
|
860
|
+
id: `prt_cerebro-context-${Date.now()}`,
|
|
861
|
+
sessionID: input.sessionID,
|
|
862
|
+
messageID: output.message.id,
|
|
863
|
+
type: "text",
|
|
864
|
+
text: injectText,
|
|
865
|
+
synthetic: true,
|
|
866
|
+
};
|
|
867
|
+
output.parts.unshift(contextPart);
|
|
868
|
+
logDebug("memoryInjectionHook block injected", {
|
|
869
|
+
sessionId: input.sessionID,
|
|
870
|
+
injectTextLen: injectText.length,
|
|
871
|
+
blockPreview: block?.slice(0, 200),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
810
874
|
|
|
811
|
-
|
|
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
|
-
}
|
|
875
|
+
injectedSessions.add(input.sessionID);
|
|
844
876
|
|
|
845
|
-
|
|
877
|
+
if (isSaveKeyword) {
|
|
878
|
+
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const newIds = newResults.map((r) => r.memory.id);
|
|
882
|
+
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
883
|
+
|
|
884
|
+
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
885
|
+
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
886
|
+
const memOther = newResults.length - memDynamic - memStatic;
|
|
887
|
+
|
|
888
|
+
let memCountMsg = "";
|
|
889
|
+
if (memDynamic > 0) memCountMsg += `Dynamic(${memDynamic}) `;
|
|
890
|
+
if (memStatic > 0) memCountMsg += `Static(${memStatic}) `;
|
|
891
|
+
if (memOther > 0) memCountMsg += `Other(${memOther}) `;
|
|
892
|
+
|
|
893
|
+
const categories = categorize(newResults);
|
|
894
|
+
const catSummary = Array.from(categories.entries())
|
|
895
|
+
.map(([label, items]) => `${label}(${items.length})`)
|
|
896
|
+
.join(" · ");
|
|
897
|
+
|
|
898
|
+
let toastTitle: string;
|
|
899
|
+
let toastMessage: string;
|
|
900
|
+
|
|
901
|
+
if (clustered) {
|
|
902
|
+
const clusterCount = clustered.cluster_summaries.length;
|
|
903
|
+
const standaloneCount = clustered.standalone_memories.length;
|
|
904
|
+
toastTitle = `🧠 Context Injected · ${clusterCount} clusters${standaloneCount > 0 ? ` · ${standaloneCount} standalone` : ""}`;
|
|
905
|
+
toastMessage = profileInjected
|
|
906
|
+
? `Profile: ${profileCountText} · Clustered memory display`
|
|
907
|
+
: `Clustered memory display`;
|
|
908
|
+
} else {
|
|
909
|
+
toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
|
|
910
|
+
toastMessage = profileInjected
|
|
911
|
+
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
912
|
+
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
846
913
|
}
|
|
914
|
+
|
|
915
|
+
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
847
916
|
}
|
|
917
|
+
}
|
|
848
918
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
919
|
+
// cache miss: fire-and-forget createRecallEvent so web UI shows the recall record
|
|
920
|
+
if (!isCacheHit && shouldRecallRes.should_recall) {
|
|
921
|
+
const results = shouldRecallRes.memories ?? [];
|
|
922
|
+
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
|
|
923
|
+
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
924
|
+
const storedMemoryIds = results.map((r) => r.memory.id);
|
|
925
|
+
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
926
|
+
const maxScore = storedMemoryIds.length > 0
|
|
927
|
+
? Math.max(...(results.map((r) => r.score) ?? [0]))
|
|
928
|
+
: 0;
|
|
929
|
+
const bgBlock = shouldRecallRes.clustered
|
|
930
|
+
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
931
|
+
: buildContextBlock(newResults, maxContentLength);
|
|
932
|
+
const items = [
|
|
933
|
+
...(results.map((r) => ({
|
|
934
|
+
memory_id: r.memory.id, score: r.score,
|
|
935
|
+
refine_relevance: r.refine_relevance, refine_reasoning: r.refine_reasoning, is_kept: true,
|
|
936
|
+
}))),
|
|
937
|
+
...(shouldRecallRes.discarded?.map((d) => ({
|
|
938
|
+
memory_id: d.memory_id, score: d.score,
|
|
939
|
+
refine_relevance: d.refine_relevance, refine_reasoning: d.refine_reasoning, is_kept: false,
|
|
940
|
+
})) ?? []),
|
|
941
|
+
];
|
|
942
|
+
client.createRecallEvent({
|
|
943
|
+
session_id: input.sessionID!, recall_type: "auto", query_text,
|
|
944
|
+
max_score: maxScore, llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
945
|
+
profile_injected: profileInjected,
|
|
946
|
+
kept_count: storedMemoryIds.length, discarded_count: storedDiscardedIds.length,
|
|
947
|
+
injected_count: newResults.length,
|
|
948
|
+
profile_content: profileInjected && profileBlock ? profileBlock : undefined,
|
|
949
|
+
injected_content: bgBlock ?? undefined,
|
|
950
|
+
items: items.length > 0 ? items : undefined,
|
|
951
|
+
}).catch((e: unknown) => {
|
|
952
|
+
logErr("memoryInjectionHook cache-miss createRecallEvent failed", { error: String(e) });
|
|
953
|
+
});
|
|
852
954
|
}
|
|
853
955
|
|
|
854
|
-
|
|
956
|
+
logDebug("memoryInjectionHook injection complete", { sessionId: input.sessionID, isCacheHit });
|
|
957
|
+
|
|
958
|
+
// ========== Phase B: fire-and-forget async fetch for NEXT round (cache hit only) ==========
|
|
959
|
+
if (isCacheHit) {
|
|
855
960
|
const bgSessionId = input.sessionID;
|
|
856
961
|
const bgQueryText = query_text;
|
|
857
962
|
const bgLastQueryText = last_query_text;
|
|
@@ -1010,6 +1115,7 @@ export function memoryInjectionHook(
|
|
|
1010
1115
|
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
1011
1116
|
}
|
|
1012
1117
|
});
|
|
1118
|
+
}
|
|
1013
1119
|
} catch (err) {
|
|
1014
1120
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1015
1121
|
if (errMsg.includes("[cerebro]")) {
|