@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.d.ts.map +1 -1
- package/dist/hooks.js +346 -213
- package/dist/hooks.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/package.json +1 -1
- package/src/hooks.ts +372 -225
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
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
706
|
+
logDebug("memoryInjectionHook cache miss, fetched synchronously", { sessionId: input.sessionID, shouldRecall: shouldRecallRes.should_recall, memCount: shouldRecallRes.memories?.length ?? 0 });
|
|
731
707
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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 (
|
|
725
|
+
logDebug("memoryInjectionHook profile injected (no-recall)", { sessionId: input.sessionID });
|
|
754
726
|
}
|
|
755
727
|
injectedSessions.add(input.sessionID);
|
|
756
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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);
|