@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/src/hooks.ts CHANGED
@@ -707,23 +707,117 @@ export function memoryInjectionHook(
707
707
  })
708
708
  : undefined;
709
709
 
710
- // ========== Phase A: synchronous path (zero await) ==========
711
- const cached = recallCache.get(input.sessionID);
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
- // Phase A: 只读 profileBlock,不更新 TTL(TTL 管理完全由 Phase B 负责)
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
- const shouldRecallRes = cached.recallResult;
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
- if (!shouldRecallRes.should_recall) {
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 from cache (no-recall)", { sessionId: input.sessionID });
834
+ logDebug("memoryInjectionHook profile injected (dedup)", { sessionId: input.sessionID });
741
835
  }
742
836
  injectedSessions.add(input.sessionID);
743
837
  } 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
- }
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
- injectedSessions.add(input.sessionID);
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
- if (isSaveKeyword) {
808
- saveKeywordDetectedSessions.delete(input.sessionID);
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
- 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
- }
875
+ injectedSessions.add(input.sessionID);
844
876
 
845
- showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
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
- logDebug("memoryInjectionHook cache hit, injection complete", { sessionId: input.sessionID });
850
- } else {
851
- logDebug("memoryInjectionHook cache miss, first message in session", { sessionId: input.sessionID });
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
- // ========== Phase B: fire-and-forget async fetch for NEXT round ==========
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]")) {