@mingxy/cerebro 1.15.4 → 1.15.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
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,28 +632,239 @@ 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: synchronous path (zero await) ==========
636
+ const cached = recallCache.get(input.sessionID);
637
+ let profileBlock = "";
638
+ let profileInjected = false;
639
+ let profileCountText = "";
640
+ if (cached) {
641
+ // Phase A: 只读 profileBlock,不更新 TTL(TTL 管理完全由 Phase B 负责)
642
+ if (cached.profileBlock) {
643
+ profileBlock = cached.profileBlock;
644
+ profileInjected = true;
645
+ profileCountText = cached.profileData?.countText ?? "";
646
+ }
647
+ const shouldRecallRes = cached.recallResult;
648
+ if (!shouldRecallRes.should_recall) {
649
+ const partsToInject = [];
650
+ if (profileBlock)
651
+ partsToInject.push(profileBlock);
652
+ if (partsToInject.length > 0) {
653
+ const injectText = partsToInject.join("\n\n");
654
+ const contextPart = {
655
+ id: `prt_cerebro-context-${Date.now()}`,
656
+ sessionID: input.sessionID,
657
+ messageID: output.message.id,
658
+ type: "text",
659
+ text: injectText,
660
+ synthetic: true,
661
+ };
662
+ output.parts.unshift(contextPart);
663
+ logDebug("memoryInjectionHook profile injected from cache (no-recall)", { sessionId: input.sessionID });
664
+ }
665
+ injectedSessions.add(input.sessionID);
666
+ }
667
+ 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);
691
+ }
692
+ 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);
767
+ }
768
+ }
769
+ logDebug("memoryInjectionHook cache hit, injection complete", { sessionId: input.sessionID });
661
770
  }
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 {
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,
810
+ });
811
+ if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
812
+ logErr("memoryInjectionHook shouldRecall returned incomplete data", {
813
+ shouldRecall: shouldRecallRes.should_recall,
814
+ hasMemories: !!shouldRecallRes.memories,
815
+ });
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;
830
+ }
831
+ }
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;
846
+ }
847
+ }
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]));
858
+ }
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;
670
868
  const items = [
671
869
  ...(shouldRecallRes.memories?.map((r) => ({
672
870
  memory_id: r.memory.id,
@@ -683,169 +881,40 @@ export function memoryInjectionHook(client, containerTags, tui, config = {}, get
683
881
  is_kept: false,
684
882
  })) ?? []),
685
883
  ];
686
- const result = await client.createRecallEvent({
687
- session_id: input.sessionID,
884
+ client.createRecallEvent({
885
+ session_id: bgSessionId,
688
886
  recall_type: "auto",
689
- query_text,
887
+ query_text: bgQueryText,
690
888
  max_score: maxScore,
691
889
  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,
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,
698
896
  items: items.length > 0 ? items : undefined,
897
+ }).catch((e) => {
898
+ logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
699
899
  });
700
- return result?.event_id;
701
- }
702
- catch (e) {
703
- logErr("memoryInjectionHook createRecallEvent failed", { error: String(e) });
704
- return undefined;
705
- }
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 });
724
900
  }
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);
729
- }
730
- return;
731
- }
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) {
739
- const partsToInject = [];
740
- if (profileBlock)
741
- partsToInject.push(profileBlock);
742
- if (partsToInject.length > 0) {
743
- const injectText = partsToInject.join("\n\n");
744
- const contextPart = {
745
- id: `prt_cerebro-context-${Date.now()}`,
746
- sessionID: input.sessionID,
747
- messageID: output.message.id,
748
- type: "text",
749
- text: injectText,
750
- synthetic: true,
751
- };
752
- output.parts.unshift(contextPart);
753
- logDebug("memoryInjectionHook profile injected (dedup path)", { sessionId: input.sessionID });
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");
909
+ }
910
+ else if (cleanMsg.includes("timed out")) {
911
+ showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
912
+ }
754
913
  }
755
- injectedSessions.add(input.sessionID);
756
- if (profileInjected && profileIsFirstInjection) {
757
- showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
914
+ else if (errMsg.includes("fetch") || errMsg.includes("network")) {
915
+ showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
758
916
  }
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
917
  });
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
- });
806
- }
807
- 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
- : `聚合记忆展示`;
841
- }
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}` : ""}`;
847
- }
848
- showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
849
918
  }
850
919
  catch (err) {
851
920
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -995,6 +1064,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
995
1064
  if (input.sessionID) {
996
1065
  sessionMessages.delete(input.sessionID);
997
1066
  profileInjectedSessions.delete(input.sessionID);
1067
+ recallCache.delete(input.sessionID);
998
1068
  firstMessages.delete(input.sessionID);
999
1069
  }
1000
1070
  return;
@@ -1024,6 +1094,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
1024
1094
  if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
1025
1095
  sessionMessages.delete(input.sessionID);
1026
1096
  profileInjectedSessions.delete(input.sessionID);
1097
+ recallCache.delete(input.sessionID);
1027
1098
  firstMessages.delete(input.sessionID);
1028
1099
  }
1029
1100
  else {
@@ -1057,6 +1128,7 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
1057
1128
  sessionMessages.delete(input.sessionID);
1058
1129
  injectedSessions.delete(input.sessionID);
1059
1130
  profileInjectedSessions.delete(input.sessionID);
1131
+ recallCache.delete(input.sessionID);
1060
1132
  firstMessages.delete(input.sessionID);
1061
1133
  if (input.sessionID) {
1062
1134
  const deleted = pendingToolCalls.delete(input.sessionID);