@mingxy/cerebro 1.15.4 → 1.15.6

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
@@ -164,6 +164,8 @@ const sessionMessages = new Map();
164
164
  const profileInjectedSessions = new Map();
165
165
  const injectedSessions = new Set();
166
166
  const compactingSummaryCooldown = new Map();
167
+ // Per-session async cache for fire-and-forget recall results
168
+ const recallCache = new Map();
167
169
  function hashString(str) {
168
170
  let hash = 0;
169
171
  for (let i = 0; i < str.length; i++) {
@@ -564,6 +566,28 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
564
566
  }
565
567
  };
566
568
  }
569
+ function buildProfileBlock(profile) {
570
+ const prefs = (profile?.static_facts ?? [])
571
+ .filter((sf) => {
572
+ const t = sf.tags ?? [];
573
+ return t.includes("preferences");
574
+ })
575
+ .map((sf) => sf.l2_content ?? sf.content ?? "")
576
+ .filter(Boolean);
577
+ const profileLines = prefs.length > 0
578
+ ? prefs.map((c) => ` · ${c}`).join("\n")
579
+ : " · (preferences queuing, will populate on next refresh)";
580
+ const block = [
581
+ "<cerebro-profile>",
582
+ profileLines,
583
+ "</cerebro-profile>",
584
+ ].join("\n");
585
+ const p = profile;
586
+ const dynamicCount = p?.dynamic_context?.length ?? 0;
587
+ const staticCount = p?.static_facts?.length ?? 0;
588
+ const countText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
589
+ return { block, countText };
590
+ }
567
591
  export function memoryInjectionHook(client, containerTags, tui, config = {}, getAgentName, directory) {
568
592
  const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
569
593
  const maxRecallResults = config.recall?.maxRecallResults ?? 10;
@@ -590,43 +614,6 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
590
614
  logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy, isSaveKeyword, similarityThreshold, maxRecallResults });
591
615
  const messages = sessionMessages.get(input.sessionID) ?? [];
592
616
  const userMessages = messages.filter((m) => m.role === "user");
593
- // --- Profile Fetch ---
594
- const profile = await client.getProfile();
595
- let profileInjected = false;
596
- let profileCountText = "";
597
- let profileBlock = "";
598
- const lastInjected = profileInjectedSessions.get(input.sessionID);
599
- const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
600
- const profileIsFirstInjection = !lastInjected;
601
- if (profile && ttlExpired) {
602
- const prefs = (profile?.static_facts ?? [])
603
- .filter((sf) => {
604
- const t = sf.tags ?? [];
605
- return t.includes("preferences");
606
- })
607
- .map((sf) => sf.l2_content ?? sf.content ?? "")
608
- .filter(Boolean);
609
- const profileLines = prefs.length > 0
610
- ? prefs.map((c) => ` · ${c}`).join("\n")
611
- : " · (preferences queuing, will populate on next refresh)";
612
- profileBlock = [
613
- "<cerebro-profile>",
614
- profileLines,
615
- "</cerebro-profile>",
616
- ].join("\n");
617
- profileInjected = true;
618
- profileInjectedSessions.set(input.sessionID, Date.now());
619
- const p = profile;
620
- const dynamicCount = p?.dynamic_context?.length ?? 0;
621
- const staticCount = p?.static_facts?.length ?? 0;
622
- profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
623
- if (profileIsFirstInjection) {
624
- logDebug("memoryInjectionHook profile ready (first)", { dynamicCount, staticCount });
625
- }
626
- else {
627
- logDebug("memoryInjectionHook profile ready (TTL)", { dynamicCount, staticCount });
628
- }
629
- }
630
617
  if (userMessages.length === 0) {
631
618
  logDebug("memoryInjectionHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
632
619
  return;
@@ -634,7 +621,7 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
634
621
  const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
635
622
  const query_text = extractUserRequest(rawQuery);
636
623
  if (!query_text) {
637
- logDebug("memoryInjectionHook filtered system injection (profile already injected above)", { rawQueryPrefix: rawQuery.slice(0, 60) });
624
+ logDebug("memoryInjectionHook filtered system injection", { rawQueryPrefix: rawQuery.slice(0, 60) });
638
625
  return;
639
626
  }
640
627
  const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
@@ -645,97 +632,82 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
645
632
  return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
646
633
  })
647
634
  : undefined;
648
- const shouldRecallRes = await client.shouldRecall(query_text, last_query_text, input.sessionID, similarityThreshold, maxRecallResults, projectTags.length > 0 ? projectTags : undefined, conversationContext && conversationContext.length > 0 ? conversationContext : undefined, {
649
- fetch_multiplier: fetchMultiplier,
650
- topk_cap_multiplier: topkCapMultiplier,
651
- mmr_jaccard_threshold: mmrJaccardThreshold,
652
- mmr_penalty_factor: mmrPenaltyFactor,
653
- phase2_multiplier: phase2Multiplier,
654
- llm_max_eval: llmMaxEval,
655
- refine_strategy: refineStrategy,
656
- refine_medium_chars: refineMediumChars,
657
- }, directory || process.env.OMEM_PROJECT_DIR);
658
- if (!shouldRecallRes) {
659
- showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
660
- return;
635
+ // ========== Phase A: unified data fetch + injection ==========
636
+ let shouldRecallRes;
637
+ let profileBlock = "";
638
+ let profileInjected = false;
639
+ let profileCountText = "";
640
+ let isCacheHit = false;
641
+ const cached = recallCache.get(input.sessionID);
642
+ if (cached) {
643
+ isCacheHit = true;
644
+ shouldRecallRes = cached.recallResult;
645
+ if (cached.profileBlock) {
646
+ profileBlock = cached.profileBlock;
647
+ profileInjected = true;
648
+ profileCountText = cached.profileData?.countText ?? "";
649
+ }
661
650
  }
662
- logDebug("memoryInjectionHook shouldRecall result", { shouldRecall: shouldRecallRes.should_recall, confidence: shouldRecallRes.confidence, memCount: shouldRecallRes.memories?.length ?? 0, discardedCount: shouldRecallRes.discarded?.length ?? 0, clustered: !!shouldRecallRes.clustered });
663
- const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
664
- const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
665
- const maxScore = storedMemoryIds.length > 0
666
- ? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
667
- : 0;
668
- const createEventAndReturn = async (injectedCount, keptCount, discardedCount, injectedContent) => {
669
- try {
670
- const items = [
671
- ...(shouldRecallRes.memories?.map((r) => ({
672
- memory_id: r.memory.id,
673
- score: r.score,
674
- refine_relevance: r.refine_relevance,
675
- refine_reasoning: r.refine_reasoning,
676
- is_kept: true,
677
- })) ?? []),
678
- ...(shouldRecallRes.discarded?.map((d) => ({
679
- memory_id: d.memory_id,
680
- score: d.score,
681
- refine_relevance: d.refine_relevance,
682
- refine_reasoning: d.refine_reasoning,
683
- is_kept: false,
684
- })) ?? []),
685
- ];
686
- const result = await client.createRecallEvent({
687
- session_id: input.sessionID,
688
- recall_type: "auto",
689
- query_text,
690
- max_score: maxScore,
691
- llm_confidence: shouldRecallRes.confidence ?? 0,
692
- profile_injected: profileInjected,
693
- kept_count: keptCount,
694
- discarded_count: discardedCount,
695
- injected_count: injectedCount,
696
- profile_content: profileInjected && profileBlock ? profileBlock : undefined,
697
- injected_content: injectedContent,
698
- items: items.length > 0 ? items : undefined,
699
- });
700
- return result?.event_id;
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;
701
669
  }
702
- catch (e) {
703
- logErr("memoryInjectionHook createRecallEvent failed", { error: String(e) });
704
- return undefined;
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
+ }
705
680
  }
706
- };
707
- // --- no-recall path: inject profile only ---
708
- if (!shouldRecallRes.should_recall) {
709
- const partsToInject = [];
710
- if (profileBlock)
711
- partsToInject.push(profileBlock);
712
- if (partsToInject.length > 0) {
713
- const injectText = partsToInject.join("\n\n");
714
- const contextPart = {
715
- id: `prt_cerebro-context-${Date.now()}`,
716
- sessionID: input.sessionID,
717
- messageID: output.message.id,
718
- type: "text",
719
- text: injectText,
720
- synthetic: true,
721
- };
722
- output.parts.unshift(contextPart);
723
- logDebug("memoryInjectionHook profile injected (no-recall path)", { sessionId: input.sessionID });
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);
724
700
  }
725
- injectedSessions.add(input.sessionID);
726
- if (profileInjected && profileIsFirstInjection) {
727
- await createEventAndReturn(0, 0, 0);
728
- showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
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;
729
705
  }
730
- return;
706
+ logDebug("memoryInjectionHook cache miss, fetched synchronously", { sessionId: input.sessionID, shouldRecall: shouldRecallRes.should_recall, memCount: shouldRecallRes.memories?.length ?? 0 });
731
707
  }
732
- const results = shouldRecallRes.memories ?? [];
733
- const clustered = shouldRecallRes.clustered;
734
- const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
735
- const newResults = results.filter((r) => !existingIds.has(r.memory.id));
736
- logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
737
- // --- dedup path: inject profile only ---
738
- if (newResults.length === 0) {
708
+ // ========== unified injection logic (cache hit + cache miss share this) ==========
709
+ if (!shouldRecallRes.should_recall) {
710
+ // no-recall path: inject profile only
739
711
  const partsToInject = [];
740
712
  if (profileBlock)
741
713
  partsToInject.push(profileBlock);
@@ -750,102 +722,260 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
750
722
  synthetic: true,
751
723
  };
752
724
  output.parts.unshift(contextPart);
753
- logDebug("memoryInjectionHook profile injected (dedup path)", { sessionId: input.sessionID });
725
+ logDebug("memoryInjectionHook profile injected (no-recall)", { sessionId: input.sessionID });
754
726
  }
755
727
  injectedSessions.add(input.sessionID);
756
- if (profileInjected && profileIsFirstInjection) {
757
- showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
758
- }
759
- return;
760
- }
761
- // --- Token Budget Calculation ---
762
- const profileChars = profileInjected ? profileBlock.length : 0;
763
- const budgetRemaining = maxContentChars - profileChars;
764
- if (budgetRemaining < 0) {
765
- logDebug("memoryInjectionHook budget overflow", { profileChars, maxContentChars, deficit: -budgetRemaining });
766
- }
767
- const itemCount = clustered
768
- ? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
769
- : newResults.length;
770
- const dynamicMaxContentLength = itemCount > 0
771
- ? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
772
- : maxContentLength;
773
- logDebug("memoryInjectionHook budget", {
774
- maxContentChars, profileChars, budgetRemaining, itemCount,
775
- configuredMax: maxContentLength, dynamicMax: dynamicMaxContentLength,
776
- });
777
- const block = clustered
778
- ? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
779
- : buildContextBlock(newResults, dynamicMaxContentLength);
780
- // ★★★ Core change: inject via output.parts.unshift + synthetic:true ★★★
781
- const partsToInject = [];
782
- if (profileBlock)
783
- partsToInject.push(profileBlock);
784
- if (block)
785
- partsToInject.push(block);
786
- if (block)
787
- partsToInject.push(FETCH_POLICY);
788
- if (isSaveKeyword)
789
- partsToInject.push(KEYWORD_NUDGE);
790
- if (partsToInject.length > 0) {
791
- const injectText = partsToInject.join("\n\n");
792
- const contextPart = {
793
- id: `prt_cerebro-context-${Date.now()}`,
794
- sessionID: input.sessionID,
795
- messageID: output.message.id,
796
- type: "text",
797
- text: injectText,
798
- synthetic: true,
799
- };
800
- output.parts.unshift(contextPart);
801
- logDebug("memoryInjectionHook block injected to output.parts", {
802
- sessionId: input.sessionID,
803
- injectTextLen: injectText.length,
804
- blockPreview: block?.slice(0, 200),
805
- });
728
+ showToast(tui, "🧠 Profile Injected", profileCountText ? `Profile: ${profileCountText} · no recall needed` : "No memory recall needed", "success", toastDelayMs);
806
729
  }
807
730
  else {
808
- logDebug("memoryInjectionHook no content to inject", { sessionId: input.sessionID });
809
- }
810
- injectedSessions.add(input.sessionID);
811
- if (isSaveKeyword) {
812
- saveKeywordDetectedSessions.delete(input.sessionID);
813
- }
814
- const newIds = newResults.map((r) => r.memory.id);
815
- injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
816
- logDebug("memoryInjectionHook injection complete", { newIds: newIds.length, clustered: !!clustered, sessionId: input.sessionID });
817
- await createEventAndReturn(newResults.length, storedMemoryIds.length, storedDiscardedIds.length, block || undefined);
818
- const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
819
- const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
820
- const memOther = newResults.length - memDynamic - memStatic;
821
- let memCountMsg = "";
822
- if (memDynamic > 0)
823
- memCountMsg += `Dynamic(${memDynamic}) `;
824
- if (memStatic > 0)
825
- memCountMsg += `Static(${memStatic}) `;
826
- if (memOther > 0)
827
- memCountMsg += `Other(${memOther}) `;
828
- const categories = categorize(newResults);
829
- const catSummary = Array.from(categories.entries())
830
- .map(([label, items]) => `${label}(${items.length})`)
831
- .join(" · ");
832
- let toastTitle;
833
- let toastMessage;
834
- if (clustered) {
835
- const clusterCount = clustered.cluster_summaries.length;
836
- const standaloneCount = clustered.standalone_memories.length;
837
- toastTitle = `🧠 Context Injected · ${clusterCount} 主题簇${standaloneCount > 0 ? ` · ${standaloneCount} 补充` : ""}`;
838
- toastMessage = profileInjected
839
- ? `Profile: ${profileCountText} · 聚合记忆展示`
840
- : `聚合记忆展示`;
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) {
737
+ const partsToInject = [];
738
+ if (profileBlock)
739
+ partsToInject.push(profileBlock);
740
+ if (partsToInject.length > 0) {
741
+ const injectText = partsToInject.join("\n\n");
742
+ const contextPart = {
743
+ id: `prt_cerebro-context-${Date.now()}`,
744
+ sessionID: input.sessionID,
745
+ messageID: output.message.id,
746
+ type: "text",
747
+ text: injectText,
748
+ synthetic: true,
749
+ };
750
+ output.parts.unshift(contextPart);
751
+ logDebug("memoryInjectionHook profile injected (dedup)", { sessionId: input.sessionID });
752
+ }
753
+ injectedSessions.add(input.sessionID);
754
+ }
755
+ else {
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`;
822
+ }
823
+ else {
824
+ toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
825
+ toastMessage = profileInjected
826
+ ? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
827
+ : `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
828
+ }
829
+ showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
830
+ }
841
831
  }
842
- else {
843
- toastTitle = `🧠 Context Injected · ${newResults.length} fragments`;
844
- toastMessage = profileInjected
845
- ? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
846
- : `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
832
+ logDebug("memoryInjectionHook injection complete", { sessionId: input.sessionID, isCacheHit });
833
+ // ========== Phase B: fire-and-forget async fetch for NEXT round (cache hit only) ==========
834
+ if (isCacheHit) {
835
+ const bgSessionId = input.sessionID;
836
+ const bgQueryText = query_text;
837
+ const bgLastQueryText = last_query_text;
838
+ const bgConversationContext = conversationContext;
839
+ const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
840
+ const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
841
+ Promise.allSettled([
842
+ client.getProfile(),
843
+ client.shouldRecall(bgQueryText, bgLastQueryText, bgSessionId, similarityThreshold, maxRecallResults, bgProjectTags, bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined, {
844
+ fetch_multiplier: fetchMultiplier,
845
+ topk_cap_multiplier: topkCapMultiplier,
846
+ mmr_jaccard_threshold: mmrJaccardThreshold,
847
+ mmr_penalty_factor: mmrPenaltyFactor,
848
+ phase2_multiplier: phase2Multiplier,
849
+ llm_max_eval: llmMaxEval,
850
+ refine_strategy: refineStrategy,
851
+ refine_medium_chars: refineMediumChars,
852
+ }, bgDirectory),
853
+ ])
854
+ .then(([profileRes, recallRes]) => {
855
+ if (recallRes.status === 'rejected') {
856
+ logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
857
+ return;
858
+ }
859
+ const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
860
+ const shouldRecallRes = recallRes.value;
861
+ if (!shouldRecallRes) {
862
+ showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
863
+ return;
864
+ }
865
+ logDebug("memoryInjectionHook background fetch complete", {
866
+ sessionId: bgSessionId,
867
+ shouldRecall: shouldRecallRes.should_recall,
868
+ confidence: shouldRecallRes.confidence,
869
+ memCount: shouldRecallRes.memories?.length ?? 0,
870
+ });
871
+ if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
872
+ logErr("memoryInjectionHook shouldRecall returned incomplete data", {
873
+ shouldRecall: shouldRecallRes.should_recall,
874
+ hasMemories: !!shouldRecallRes.memories,
875
+ });
876
+ return;
877
+ }
878
+ let bgProfileBlock = "";
879
+ let bgProfileCountText = "";
880
+ let bgProfileInjected = false;
881
+ if (profile) {
882
+ const lastInjected = profileInjectedSessions.get(bgSessionId);
883
+ const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
884
+ if (ttlExpired) {
885
+ const built = buildProfileBlock(profile);
886
+ if (built) {
887
+ bgProfileBlock = built.block;
888
+ bgProfileCountText = built.countText;
889
+ bgProfileInjected = true;
890
+ }
891
+ }
892
+ }
893
+ recallCache.set(bgSessionId, {
894
+ profileBlock: bgProfileBlock,
895
+ recallResult: shouldRecallRes,
896
+ profileData: { countText: bgProfileCountText },
897
+ timestamp: Date.now(),
898
+ });
899
+ if (recallCache.size > 50) {
900
+ let oldestKey = null;
901
+ let oldestTime = Infinity;
902
+ for (const [k, v] of recallCache) {
903
+ if (v.timestamp < oldestTime) {
904
+ oldestTime = v.timestamp;
905
+ oldestKey = k;
906
+ }
907
+ }
908
+ if (oldestKey)
909
+ recallCache.delete(oldestKey);
910
+ }
911
+ if (shouldRecallRes.should_recall) {
912
+ const results = shouldRecallRes.memories ?? [];
913
+ const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set();
914
+ const newResults = results.filter((r) => !existingIds.has(r.memory.id));
915
+ if (newResults.length > 0) {
916
+ const newIds = newResults.map((r) => r.memory.id);
917
+ injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
918
+ }
919
+ const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
920
+ const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
921
+ const maxScore = storedMemoryIds.length > 0
922
+ ? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
923
+ : 0;
924
+ const bgBlock = shouldRecallRes.clustered
925
+ ? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
926
+ : buildContextBlock(newResults, maxContentLength);
927
+ const bgInjectedContent = bgBlock ?? undefined;
928
+ const items = [
929
+ ...(shouldRecallRes.memories?.map((r) => ({
930
+ memory_id: r.memory.id,
931
+ score: r.score,
932
+ refine_relevance: r.refine_relevance,
933
+ refine_reasoning: r.refine_reasoning,
934
+ is_kept: true,
935
+ })) ?? []),
936
+ ...(shouldRecallRes.discarded?.map((d) => ({
937
+ memory_id: d.memory_id,
938
+ score: d.score,
939
+ refine_relevance: d.refine_relevance,
940
+ refine_reasoning: d.refine_reasoning,
941
+ is_kept: false,
942
+ })) ?? []),
943
+ ];
944
+ client.createRecallEvent({
945
+ session_id: bgSessionId,
946
+ recall_type: "auto",
947
+ query_text: bgQueryText,
948
+ max_score: maxScore,
949
+ llm_confidence: shouldRecallRes.confidence ?? 0,
950
+ profile_injected: bgProfileInjected,
951
+ kept_count: storedMemoryIds.length,
952
+ discarded_count: storedDiscardedIds.length,
953
+ injected_count: newResults.length,
954
+ profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
955
+ injected_content: bgInjectedContent,
956
+ items: items.length > 0 ? items : undefined,
957
+ }).catch((e) => {
958
+ logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
959
+ });
960
+ }
961
+ })
962
+ .catch((err) => {
963
+ const errMsg = err instanceof Error ? err.message : String(err);
964
+ logErr("memoryInjectionHook background fetch failed", { error: errMsg });
965
+ if (errMsg.includes("[cerebro]")) {
966
+ const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
967
+ if (cleanMsg.startsWith("500")) {
968
+ showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
969
+ }
970
+ else if (cleanMsg.includes("timed out")) {
971
+ showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
972
+ }
973
+ }
974
+ else if (errMsg.includes("fetch") || errMsg.includes("network")) {
975
+ showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
976
+ }
977
+ });
847
978
  }
848
- showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
849
979
  }
850
980
  catch (err) {
851
981
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -995,6 +1125,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
995
1125
  if (input.sessionID) {
996
1126
  sessionMessages.delete(input.sessionID);
997
1127
  profileInjectedSessions.delete(input.sessionID);
1128
+ recallCache.delete(input.sessionID);
998
1129
  firstMessages.delete(input.sessionID);
999
1130
  }
1000
1131
  return;
@@ -1024,6 +1155,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
1024
1155
  if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
1025
1156
  sessionMessages.delete(input.sessionID);
1026
1157
  profileInjectedSessions.delete(input.sessionID);
1158
+ recallCache.delete(input.sessionID);
1027
1159
  firstMessages.delete(input.sessionID);
1028
1160
  }
1029
1161
  else {
@@ -1057,6 +1189,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
1057
1189
  sessionMessages.delete(input.sessionID);
1058
1190
  injectedSessions.delete(input.sessionID);
1059
1191
  profileInjectedSessions.delete(input.sessionID);
1192
+ recallCache.delete(input.sessionID);
1060
1193
  firstMessages.delete(input.sessionID);
1061
1194
  if (input.sessionID) {
1062
1195
  const deleted = pendingToolCalls.delete(input.sessionID);