@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.js CHANGED
@@ -632,20 +632,108 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
632
632
  return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
633
633
  })
634
634
  : undefined;
635
- // ========== Phase A: synchronous path (zero await) ==========
636
- const cached = recallCache.get(input.sessionID);
635
+ // ========== Phase A: unified data fetch + injection ==========
636
+ let shouldRecallRes;
637
637
  let profileBlock = "";
638
638
  let profileInjected = false;
639
639
  let profileCountText = "";
640
+ let isCacheHit = false;
641
+ const cached = recallCache.get(input.sessionID);
640
642
  if (cached) {
641
- // Phase A: 只读 profileBlock,不更新 TTL(TTL 管理完全由 Phase B 负责)
643
+ isCacheHit = true;
644
+ shouldRecallRes = cached.recallResult;
642
645
  if (cached.profileBlock) {
643
646
  profileBlock = cached.profileBlock;
644
647
  profileInjected = true;
645
648
  profileCountText = cached.profileData?.countText ?? "";
646
649
  }
647
- const shouldRecallRes = cached.recallResult;
648
- if (!shouldRecallRes.should_recall) {
650
+ }
651
+ else {
652
+ // cache miss: synchronous await (first message takes 5-8s, but gets injection)
653
+ const [profile, recallRes] = await Promise.all([
654
+ client.getProfile(),
655
+ client.shouldRecall(query_text, last_query_text, input.sessionID, similarityThreshold, maxRecallResults, projectTags.length > 0 ? projectTags : undefined, conversationContext && conversationContext.length > 0 ? conversationContext : undefined, {
656
+ fetch_multiplier: fetchMultiplier,
657
+ topk_cap_multiplier: topkCapMultiplier,
658
+ mmr_jaccard_threshold: mmrJaccardThreshold,
659
+ mmr_penalty_factor: mmrPenaltyFactor,
660
+ phase2_multiplier: phase2Multiplier,
661
+ llm_max_eval: llmMaxEval,
662
+ refine_strategy: refineStrategy,
663
+ refine_medium_chars: refineMediumChars,
664
+ }, directory || process.env.OMEM_PROJECT_DIR),
665
+ ]);
666
+ if (!recallRes) {
667
+ showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API", "error", toastDelayMs);
668
+ return;
669
+ }
670
+ shouldRecallRes = recallRes;
671
+ // build profile block
672
+ if (profile) {
673
+ const built = buildProfileBlock(profile);
674
+ if (built) {
675
+ profileBlock = built.block;
676
+ profileCountText = built.countText;
677
+ profileInjected = true;
678
+ profileInjectedSessions.set(input.sessionID, Date.now());
679
+ }
680
+ }
681
+ // write cache for next round
682
+ recallCache.set(input.sessionID, {
683
+ profileBlock,
684
+ recallResult: shouldRecallRes,
685
+ profileData: { countText: profileCountText },
686
+ timestamp: Date.now(),
687
+ });
688
+ // LRU eviction
689
+ if (recallCache.size > 50) {
690
+ let oldestKey = null;
691
+ let oldestTime = Infinity;
692
+ for (const [k, v] of recallCache) {
693
+ if (v.timestamp < oldestTime) {
694
+ oldestTime = v.timestamp;
695
+ oldestKey = k;
696
+ }
697
+ }
698
+ if (oldestKey)
699
+ recallCache.delete(oldestKey);
700
+ }
701
+ // defensive check
702
+ if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
703
+ logErr("memoryInjectionHook shouldRecall returned incomplete data", { shouldRecall: shouldRecallRes.should_recall, hasMemories: !!shouldRecallRes.memories });
704
+ return;
705
+ }
706
+ logDebug("memoryInjectionHook cache miss, fetched synchronously", { sessionId: input.sessionID, shouldRecall: shouldRecallRes.should_recall, memCount: shouldRecallRes.memories?.length ?? 0 });
707
+ }
708
+ // ========== unified injection logic (cache hit + cache miss share this) ==========
709
+ if (!shouldRecallRes.should_recall) {
710
+ // no-recall path: inject profile only
711
+ const partsToInject = [];
712
+ if (profileBlock)
713
+ partsToInject.push(profileBlock);
714
+ if (partsToInject.length > 0) {
715
+ const injectText = partsToInject.join("\n\n");
716
+ const contextPart = {
717
+ id: `prt_cerebro-context-${Date.now()}`,
718
+ sessionID: input.sessionID,
719
+ messageID: output.message.id,
720
+ type: "text",
721
+ text: injectText,
722
+ synthetic: true,
723
+ };
724
+ output.parts.unshift(contextPart);
725
+ logDebug("memoryInjectionHook profile injected (no-recall)", { sessionId: input.sessionID });
726
+ }
727
+ injectedSessions.add(input.sessionID);
728
+ showToast(tui, "🧠 Profile Injected", profileCountText ? `Profile: ${profileCountText} · no recall needed` : "No memory recall needed", "success", toastDelayMs);
729
+ }
730
+ else {
731
+ const results = shouldRecallRes.memories ?? [];
732
+ const clustered = shouldRecallRes.clustered;
733
+ const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
734
+ const newResults = results.filter((r) => !existingIds.has(r.memory.id));
735
+ logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
736
+ if (newResults.length === 0) {
649
737
  const partsToInject = [];
650
738
  if (profileBlock)
651
739
  partsToInject.push(profileBlock);
@@ -660,261 +748,270 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
660
748
  synthetic: true,
661
749
  };
662
750
  output.parts.unshift(contextPart);
663
- logDebug("memoryInjectionHook profile injected from cache (no-recall)", { sessionId: input.sessionID });
751
+ logDebug("memoryInjectionHook profile injected (dedup)", { sessionId: input.sessionID });
664
752
  }
665
753
  injectedSessions.add(input.sessionID);
666
754
  }
667
755
  else {
668
- const results = shouldRecallRes.memories ?? [];
669
- const clustered = shouldRecallRes.clustered;
670
- const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
671
- const newResults = results.filter((r) => !existingIds.has(r.memory.id));
672
- logDebug("memoryInjectionHook dedup (cached)", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
673
- if (newResults.length === 0) {
674
- const partsToInject = [];
675
- if (profileBlock)
676
- partsToInject.push(profileBlock);
677
- if (partsToInject.length > 0) {
678
- const injectText = partsToInject.join("\n\n");
679
- const contextPart = {
680
- id: `prt_cerebro-context-${Date.now()}`,
681
- sessionID: input.sessionID,
682
- messageID: output.message.id,
683
- type: "text",
684
- text: injectText,
685
- synthetic: true,
686
- };
687
- output.parts.unshift(contextPart);
688
- logDebug("memoryInjectionHook profile injected from cache (dedup)", { sessionId: input.sessionID });
689
- }
690
- injectedSessions.add(input.sessionID);
756
+ const profileChars = profileInjected ? profileBlock.length : 0;
757
+ const budgetRemaining = maxContentChars - profileChars;
758
+ const itemCount = clustered
759
+ ? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
760
+ : newResults.length;
761
+ const dynamicMaxContentLength = itemCount > 0
762
+ ? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
763
+ : maxContentLength;
764
+ const block = clustered
765
+ ? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
766
+ : buildContextBlock(newResults, dynamicMaxContentLength);
767
+ const partsToInject = [];
768
+ if (block)
769
+ partsToInject.push(block);
770
+ if (block)
771
+ partsToInject.push(FETCH_POLICY);
772
+ if (profileBlock)
773
+ partsToInject.push(profileBlock);
774
+ if (isSaveKeyword)
775
+ partsToInject.push(KEYWORD_NUDGE);
776
+ if (partsToInject.length > 0) {
777
+ const injectText = partsToInject.join("\n\n");
778
+ const contextPart = {
779
+ id: `prt_cerebro-context-${Date.now()}`,
780
+ sessionID: input.sessionID,
781
+ messageID: output.message.id,
782
+ type: "text",
783
+ text: injectText,
784
+ synthetic: true,
785
+ };
786
+ output.parts.unshift(contextPart);
787
+ logDebug("memoryInjectionHook block injected", {
788
+ sessionId: input.sessionID,
789
+ injectTextLen: injectText.length,
790
+ blockPreview: block?.slice(0, 200),
791
+ });
792
+ }
793
+ injectedSessions.add(input.sessionID);
794
+ if (isSaveKeyword) {
795
+ saveKeywordDetectedSessions.delete(input.sessionID);
796
+ }
797
+ const newIds = newResults.map((r) => r.memory.id);
798
+ injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
799
+ const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
800
+ const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
801
+ const memOther = newResults.length - memDynamic - memStatic;
802
+ let memCountMsg = "";
803
+ if (memDynamic > 0)
804
+ memCountMsg += `Dynamic(${memDynamic}) `;
805
+ if (memStatic > 0)
806
+ memCountMsg += `Static(${memStatic}) `;
807
+ if (memOther > 0)
808
+ memCountMsg += `Other(${memOther}) `;
809
+ const categories = categorize(newResults);
810
+ const catSummary = Array.from(categories.entries())
811
+ .map(([label, items]) => `${label}(${items.length})`)
812
+ .join(" · ");
813
+ let toastTitle;
814
+ let toastMessage;
815
+ if (clustered) {
816
+ const clusterCount = clustered.cluster_summaries.length;
817
+ const standaloneCount = clustered.standalone_memories.length;
818
+ toastTitle = `🧠 Context Injected · ${clusterCount} clusters${standaloneCount > 0 ? ` · ${standaloneCount} standalone` : ""}`;
819
+ toastMessage = profileInjected
820
+ ? `Profile: ${profileCountText} · Clustered memory display`
821
+ : `Clustered memory display`;
691
822
  }
692
823
  else {
693
- const profileChars = profileInjected ? profileBlock.length : 0;
694
- const budgetRemaining = maxContentChars - profileChars;
695
- const itemCount = clustered
696
- ? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
697
- : newResults.length;
698
- const dynamicMaxContentLength = itemCount > 0
699
- ? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
700
- : maxContentLength;
701
- const block = clustered
702
- ? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
703
- : buildContextBlock(newResults, dynamicMaxContentLength);
704
- const partsToInject = [];
705
- if (block)
706
- partsToInject.push(block);
707
- if (block)
708
- partsToInject.push(FETCH_POLICY);
709
- if (profileBlock)
710
- partsToInject.push(profileBlock);
711
- if (isSaveKeyword)
712
- partsToInject.push(KEYWORD_NUDGE);
713
- if (partsToInject.length > 0) {
714
- const injectText = partsToInject.join("\n\n");
715
- const contextPart = {
716
- id: `prt_cerebro-context-${Date.now()}`,
717
- sessionID: input.sessionID,
718
- messageID: output.message.id,
719
- type: "text",
720
- text: injectText,
721
- synthetic: true,
722
- };
723
- output.parts.unshift(contextPart);
724
- logDebug("memoryInjectionHook block injected from cache", {
725
- sessionId: input.sessionID,
726
- injectTextLen: injectText.length,
727
- blockPreview: block?.slice(0, 200),
728
- });
729
- }
730
- injectedSessions.add(input.sessionID);
731
- if (isSaveKeyword) {
732
- saveKeywordDetectedSessions.delete(input.sessionID);
733
- }
734
- const newIds = newResults.map((r) => r.memory.id);
735
- injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
736
- const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
737
- const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
738
- const memOther = newResults.length - memDynamic - memStatic;
739
- let memCountMsg = "";
740
- if (memDynamic > 0)
741
- memCountMsg += `Dynamic(${memDynamic}) `;
742
- if (memStatic > 0)
743
- memCountMsg += `Static(${memStatic}) `;
744
- if (memOther > 0)
745
- memCountMsg += `Other(${memOther}) `;
746
- const categories = categorize(newResults);
747
- const catSummary = Array.from(categories.entries())
748
- .map(([label, items]) => `${label}(${items.length})`)
749
- .join(" · ");
750
- let toastTitle;
751
- let toastMessage;
752
- if (clustered) {
753
- const clusterCount = clustered.cluster_summaries.length;
754
- const standaloneCount = clustered.standalone_memories.length;
755
- toastTitle = `🧠 Context Injected · ${clusterCount} 主题簇${standaloneCount > 0 ? ` · ${standaloneCount} 补充` : ""}`;
756
- toastMessage = profileInjected
757
- ? `Profile: ${profileCountText} · 聚合记忆展示`
758
- : `聚合记忆展示`;
759
- }
760
- else {
761
- toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
762
- toastMessage = profileInjected
763
- ? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
764
- : `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
765
- }
766
- showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
824
+ toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
825
+ toastMessage = profileInjected
826
+ ? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
827
+ : `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
767
828
  }
829
+ showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
768
830
  }
769
- logDebug("memoryInjectionHook cache hit, injection complete", { sessionId: input.sessionID });
770
831
  }
771
- else {
772
- logDebug("memoryInjectionHook cache miss, first message in session", { sessionId: input.sessionID });
773
- }
774
- // ========== Phase B: fire-and-forget async fetch for NEXT round ==========
775
- const bgSessionId = input.sessionID;
776
- const bgQueryText = query_text;
777
- const bgLastQueryText = last_query_text;
778
- const bgConversationContext = conversationContext;
779
- const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
780
- const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
781
- Promise.allSettled([
782
- client.getProfile(),
783
- client.shouldRecall(bgQueryText, bgLastQueryText, bgSessionId, similarityThreshold, maxRecallResults, bgProjectTags, bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined, {
784
- fetch_multiplier: fetchMultiplier,
785
- topk_cap_multiplier: topkCapMultiplier,
786
- mmr_jaccard_threshold: mmrJaccardThreshold,
787
- mmr_penalty_factor: mmrPenaltyFactor,
788
- phase2_multiplier: phase2Multiplier,
789
- llm_max_eval: llmMaxEval,
790
- refine_strategy: refineStrategy,
791
- refine_medium_chars: refineMediumChars,
792
- }, bgDirectory),
793
- ])
794
- .then(([profileRes, recallRes]) => {
795
- if (recallRes.status === 'rejected') {
796
- logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
797
- return;
798
- }
799
- const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
800
- const shouldRecallRes = recallRes.value;
801
- if (!shouldRecallRes) {
802
- showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
803
- return;
804
- }
805
- logDebug("memoryInjectionHook background fetch complete", {
806
- sessionId: bgSessionId,
807
- shouldRecall: shouldRecallRes.should_recall,
808
- confidence: shouldRecallRes.confidence,
809
- memCount: shouldRecallRes.memories?.length ?? 0,
832
+ // cache miss: fire-and-forget createRecallEvent so web UI shows the recall record
833
+ if (!isCacheHit && shouldRecallRes.should_recall) {
834
+ const results = shouldRecallRes.memories ?? [];
835
+ const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
836
+ const newResults = results.filter((r) => !existingIds.has(r.memory.id));
837
+ const storedMemoryIds = results.map((r) => r.memory.id);
838
+ const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
839
+ const maxScore = storedMemoryIds.length > 0
840
+ ? Math.max(...(results.map((r) => r.score) ?? [0]))
841
+ : 0;
842
+ const bgBlock = shouldRecallRes.clustered
843
+ ? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
844
+ : buildContextBlock(newResults, maxContentLength);
845
+ const items = [
846
+ ...(results.map((r) => ({
847
+ memory_id: r.memory.id, score: r.score,
848
+ refine_relevance: r.refine_relevance, refine_reasoning: r.refine_reasoning, is_kept: true,
849
+ }))),
850
+ ...(shouldRecallRes.discarded?.map((d) => ({
851
+ memory_id: d.memory_id, score: d.score,
852
+ refine_relevance: d.refine_relevance, refine_reasoning: d.refine_reasoning, is_kept: false,
853
+ })) ?? []),
854
+ ];
855
+ client.createRecallEvent({
856
+ session_id: input.sessionID, recall_type: "auto", query_text,
857
+ max_score: maxScore, llm_confidence: shouldRecallRes.confidence ?? 0,
858
+ profile_injected: profileInjected,
859
+ kept_count: storedMemoryIds.length, discarded_count: storedDiscardedIds.length,
860
+ injected_count: newResults.length,
861
+ profile_content: profileInjected && profileBlock ? profileBlock : undefined,
862
+ injected_content: bgBlock ?? undefined,
863
+ items: items.length > 0 ? items : undefined,
864
+ }).catch((e) => {
865
+ logErr("memoryInjectionHook cache-miss createRecallEvent failed", { error: String(e) });
810
866
  });
811
- if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
812
- logErr("memoryInjectionHook shouldRecall returned incomplete data", {
867
+ }
868
+ logDebug("memoryInjectionHook injection complete", { sessionId: input.sessionID, isCacheHit });
869
+ // ========== Phase B: fire-and-forget async fetch for NEXT round (cache hit only) ==========
870
+ if (isCacheHit) {
871
+ const bgSessionId = input.sessionID;
872
+ const bgQueryText = query_text;
873
+ const bgLastQueryText = last_query_text;
874
+ const bgConversationContext = conversationContext;
875
+ const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
876
+ const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
877
+ Promise.allSettled([
878
+ client.getProfile(),
879
+ client.shouldRecall(bgQueryText, bgLastQueryText, bgSessionId, similarityThreshold, maxRecallResults, bgProjectTags, bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined, {
880
+ fetch_multiplier: fetchMultiplier,
881
+ topk_cap_multiplier: topkCapMultiplier,
882
+ mmr_jaccard_threshold: mmrJaccardThreshold,
883
+ mmr_penalty_factor: mmrPenaltyFactor,
884
+ phase2_multiplier: phase2Multiplier,
885
+ llm_max_eval: llmMaxEval,
886
+ refine_strategy: refineStrategy,
887
+ refine_medium_chars: refineMediumChars,
888
+ }, bgDirectory),
889
+ ])
890
+ .then(([profileRes, recallRes]) => {
891
+ if (recallRes.status === 'rejected') {
892
+ logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
893
+ return;
894
+ }
895
+ const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
896
+ const shouldRecallRes = recallRes.value;
897
+ if (!shouldRecallRes) {
898
+ showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
899
+ return;
900
+ }
901
+ logDebug("memoryInjectionHook background fetch complete", {
902
+ sessionId: bgSessionId,
813
903
  shouldRecall: shouldRecallRes.should_recall,
814
- hasMemories: !!shouldRecallRes.memories,
904
+ confidence: shouldRecallRes.confidence,
905
+ memCount: shouldRecallRes.memories?.length ?? 0,
815
906
  });
816
- return;
817
- }
818
- let bgProfileBlock = "";
819
- let bgProfileCountText = "";
820
- let bgProfileInjected = false;
821
- if (profile) {
822
- const lastInjected = profileInjectedSessions.get(bgSessionId);
823
- const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
824
- if (ttlExpired) {
825
- const built = buildProfileBlock(profile);
826
- if (built) {
827
- bgProfileBlock = built.block;
828
- bgProfileCountText = built.countText;
829
- bgProfileInjected = true;
907
+ if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
908
+ logErr("memoryInjectionHook shouldRecall returned incomplete data", {
909
+ shouldRecall: shouldRecallRes.should_recall,
910
+ hasMemories: !!shouldRecallRes.memories,
911
+ });
912
+ return;
913
+ }
914
+ let bgProfileBlock = "";
915
+ let bgProfileCountText = "";
916
+ let bgProfileInjected = false;
917
+ if (profile) {
918
+ const lastInjected = profileInjectedSessions.get(bgSessionId);
919
+ const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
920
+ if (ttlExpired) {
921
+ const built = buildProfileBlock(profile);
922
+ if (built) {
923
+ bgProfileBlock = built.block;
924
+ bgProfileCountText = built.countText;
925
+ bgProfileInjected = true;
926
+ }
830
927
  }
831
928
  }
832
- }
833
- recallCache.set(bgSessionId, {
834
- profileBlock: bgProfileBlock,
835
- recallResult: shouldRecallRes,
836
- profileData: { countText: bgProfileCountText },
837
- timestamp: Date.now(),
838
- });
839
- if (recallCache.size > 50) {
840
- let oldestKey = null;
841
- let oldestTime = Infinity;
842
- for (const [k, v] of recallCache) {
843
- if (v.timestamp < oldestTime) {
844
- oldestTime = v.timestamp;
845
- oldestKey = k;
929
+ recallCache.set(bgSessionId, {
930
+ profileBlock: bgProfileBlock,
931
+ recallResult: shouldRecallRes,
932
+ profileData: { countText: bgProfileCountText },
933
+ timestamp: Date.now(),
934
+ });
935
+ if (recallCache.size > 50) {
936
+ let oldestKey = null;
937
+ let oldestTime = Infinity;
938
+ for (const [k, v] of recallCache) {
939
+ if (v.timestamp < oldestTime) {
940
+ oldestTime = v.timestamp;
941
+ oldestKey = k;
942
+ }
846
943
  }
944
+ if (oldestKey)
945
+ recallCache.delete(oldestKey);
847
946
  }
848
- if (oldestKey)
849
- recallCache.delete(oldestKey);
850
- }
851
- if (shouldRecallRes.should_recall) {
852
- const results = shouldRecallRes.memories ?? [];
853
- const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set();
854
- const newResults = results.filter((r) => !existingIds.has(r.memory.id));
855
- if (newResults.length > 0) {
856
- const newIds = newResults.map((r) => r.memory.id);
857
- injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
947
+ if (shouldRecallRes.should_recall) {
948
+ const results = shouldRecallRes.memories ?? [];
949
+ const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set();
950
+ const newResults = results.filter((r) => !existingIds.has(r.memory.id));
951
+ if (newResults.length > 0) {
952
+ const newIds = newResults.map((r) => r.memory.id);
953
+ injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
954
+ }
955
+ const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
956
+ const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
957
+ const maxScore = storedMemoryIds.length > 0
958
+ ? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
959
+ : 0;
960
+ const bgBlock = shouldRecallRes.clustered
961
+ ? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
962
+ : buildContextBlock(newResults, maxContentLength);
963
+ const bgInjectedContent = bgBlock ?? undefined;
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
+ client.createRecallEvent({
981
+ session_id: bgSessionId,
982
+ recall_type: "auto",
983
+ query_text: bgQueryText,
984
+ max_score: maxScore,
985
+ llm_confidence: shouldRecallRes.confidence ?? 0,
986
+ profile_injected: bgProfileInjected,
987
+ kept_count: storedMemoryIds.length,
988
+ discarded_count: storedDiscardedIds.length,
989
+ injected_count: newResults.length,
990
+ profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
991
+ injected_content: bgInjectedContent,
992
+ items: items.length > 0 ? items : undefined,
993
+ }).catch((e) => {
994
+ logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
995
+ });
858
996
  }
859
- const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
860
- const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
861
- const maxScore = storedMemoryIds.length > 0
862
- ? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
863
- : 0;
864
- const bgBlock = shouldRecallRes.clustered
865
- ? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
866
- : buildContextBlock(newResults, maxContentLength);
867
- const bgInjectedContent = bgBlock ?? undefined;
868
- const items = [
869
- ...(shouldRecallRes.memories?.map((r) => ({
870
- memory_id: r.memory.id,
871
- score: r.score,
872
- refine_relevance: r.refine_relevance,
873
- refine_reasoning: r.refine_reasoning,
874
- is_kept: true,
875
- })) ?? []),
876
- ...(shouldRecallRes.discarded?.map((d) => ({
877
- memory_id: d.memory_id,
878
- score: d.score,
879
- refine_relevance: d.refine_relevance,
880
- refine_reasoning: d.refine_reasoning,
881
- is_kept: false,
882
- })) ?? []),
883
- ];
884
- client.createRecallEvent({
885
- session_id: bgSessionId,
886
- recall_type: "auto",
887
- query_text: bgQueryText,
888
- max_score: maxScore,
889
- llm_confidence: shouldRecallRes.confidence ?? 0,
890
- profile_injected: bgProfileInjected,
891
- kept_count: storedMemoryIds.length,
892
- discarded_count: storedDiscardedIds.length,
893
- injected_count: newResults.length,
894
- profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
895
- injected_content: bgInjectedContent,
896
- items: items.length > 0 ? items : undefined,
897
- }).catch((e) => {
898
- logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
899
- });
900
- }
901
- })
902
- .catch((err) => {
903
- const errMsg = err instanceof Error ? err.message : String(err);
904
- logErr("memoryInjectionHook background fetch failed", { error: errMsg });
905
- if (errMsg.includes("[cerebro]")) {
906
- const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
907
- if (cleanMsg.startsWith("500")) {
908
- showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
997
+ })
998
+ .catch((err) => {
999
+ const errMsg = err instanceof Error ? err.message : String(err);
1000
+ logErr("memoryInjectionHook background fetch failed", { error: errMsg });
1001
+ if (errMsg.includes("[cerebro]")) {
1002
+ const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
1003
+ if (cleanMsg.startsWith("500")) {
1004
+ showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
1005
+ }
1006
+ else if (cleanMsg.includes("timed out")) {
1007
+ showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
1008
+ }
909
1009
  }
910
- else if (cleanMsg.includes("timed out")) {
911
- showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
1010
+ else if (errMsg.includes("fetch") || errMsg.includes("network")) {
1011
+ showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
912
1012
  }
913
- }
914
- else if (errMsg.includes("fetch") || errMsg.includes("network")) {
915
- showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
916
- }
917
- });
1013
+ });
1014
+ }
918
1015
  }
919
1016
  catch (err) {
920
1017
  const errMsg = err instanceof Error ? err.message : String(err);