@mingxy/cerebro 1.15.13 → 1.15.14
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/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +2 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -8
- package/dist/config.js.map +1 -1
- package/dist/hooks.d.ts +3 -34
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +118 -727
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -90
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/package.json +1 -1
- package/schema.json +7 -24
- package/src/client.ts +9 -0
- package/src/config.ts +5 -14
- package/src/hooks.ts +143 -799
- package/src/index.ts +8 -90
package/dist/hooks.js
CHANGED
|
@@ -116,7 +116,7 @@ async function detectProjectName(rootPath) {
|
|
|
116
116
|
}
|
|
117
117
|
return result;
|
|
118
118
|
}
|
|
119
|
-
function showToast(tui, title, message, variant = "info", delayMs = 7000) {
|
|
119
|
+
export function showToast(tui, title, message, variant = "info", delayMs = 7000) {
|
|
120
120
|
if (!tui)
|
|
121
121
|
return;
|
|
122
122
|
setTimeout(() => {
|
|
@@ -162,19 +162,7 @@ const injectedMemoryIds = new Map();
|
|
|
162
162
|
const firstMessages = new Map();
|
|
163
163
|
const sessionMessages = new Map();
|
|
164
164
|
export const profileInjectedSessions = new Map();
|
|
165
|
-
const
|
|
166
|
-
const compactingSummaryCooldown = new Map();
|
|
167
|
-
// Per-session async cache for fire-and-forget recall results
|
|
168
|
-
export const recallCache = new Map();
|
|
169
|
-
function hashString(str) {
|
|
170
|
-
let hash = 0;
|
|
171
|
-
for (let i = 0; i < str.length; i++) {
|
|
172
|
-
const char = str.charCodeAt(i);
|
|
173
|
-
hash = ((hash << 5) - hash) + char;
|
|
174
|
-
hash |= 0;
|
|
175
|
-
}
|
|
176
|
-
return hash.toString(36);
|
|
177
|
-
}
|
|
165
|
+
const summarizedSessions = new Set();
|
|
178
166
|
function formatRelativeAge(isoDate) {
|
|
179
167
|
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
180
168
|
const minutes = Math.floor(diffMs / 60_000);
|
|
@@ -323,41 +311,27 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
|
|
|
323
311
|
logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy, refineMediumChars });
|
|
324
312
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
325
313
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
326
|
-
// --- Profile Fetch (
|
|
327
|
-
const
|
|
314
|
+
// --- Profile Fetch (V2 inject API with TTL gate) ---
|
|
315
|
+
const profileTtlMs = config.profile?.ttlMs ?? 300000; // default 5 minutes
|
|
316
|
+
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
317
|
+
const profileTtlExpired = !lastInjected || (Date.now() - lastInjected > profileTtlMs);
|
|
318
|
+
let profileBlock = "";
|
|
328
319
|
let profileInjected = false;
|
|
329
320
|
let profileCountText = "";
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
.map((sf) => sf.l2_content ?? sf.content ?? "")
|
|
341
|
-
.filter(Boolean);
|
|
342
|
-
const profileLines = prefs.length > 0
|
|
343
|
-
? prefs.map((c) => ` · ${c}`).join("\n")
|
|
344
|
-
: " · (preferences queuing, will populate on next refresh)";
|
|
345
|
-
profileBlock = [
|
|
346
|
-
"<cerebro-profile>",
|
|
347
|
-
profileLines,
|
|
348
|
-
"</cerebro-profile>",
|
|
349
|
-
].join("\n");
|
|
350
|
-
profileInjected = true;
|
|
351
|
-
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
352
|
-
const p = profile;
|
|
353
|
-
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
354
|
-
const staticCount = p?.static_facts?.length ?? 0;
|
|
355
|
-
profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
356
|
-
if (isFirstInjection) {
|
|
357
|
-
logDebug("autoRecallHook profile ready (first)", { dynamicCount, staticCount });
|
|
321
|
+
if (profileTtlExpired) {
|
|
322
|
+
try {
|
|
323
|
+
const injection = await client.getInjection(directory || process.env.OMEM_PROJECT_DIR);
|
|
324
|
+
if (injection?.content) {
|
|
325
|
+
profileBlock = injection.content;
|
|
326
|
+
profileCountText = `${injection.preference_count} preferences`;
|
|
327
|
+
profileInjected = true;
|
|
328
|
+
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
329
|
+
logDebug("autoRecallHook profile ready (V2 injection)", { preferenceCount: injection.preference_count, estimatedTokens: injection.estimated_tokens });
|
|
330
|
+
}
|
|
358
331
|
}
|
|
359
|
-
|
|
360
|
-
|
|
332
|
+
catch (e) {
|
|
333
|
+
logErr("autoRecallHook getInjection failed, skipping profile", { error: String(e) });
|
|
334
|
+
// profile failure does not block shouldRecall
|
|
361
335
|
}
|
|
362
336
|
}
|
|
363
337
|
// After compacting, sessionMessages is cleared but firstMessages gets repopulated
|
|
@@ -444,8 +418,7 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
|
|
|
444
418
|
appendToSystem(output.system, profileBlock);
|
|
445
419
|
logDebug("autoRecallHook profile injected (no-recall path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
|
|
446
420
|
}
|
|
447
|
-
if (profileInjected &&
|
|
448
|
-
await createEventAndReturn(0, 0, 0);
|
|
421
|
+
if (profileInjected && !lastInjected) {
|
|
449
422
|
showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
|
|
450
423
|
}
|
|
451
424
|
return;
|
|
@@ -460,7 +433,7 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
|
|
|
460
433
|
appendToSystem(output.system, profileBlock);
|
|
461
434
|
logDebug("autoRecallHook profile injected (dedup path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
|
|
462
435
|
}
|
|
463
|
-
if (profileInjected &&
|
|
436
|
+
if (profileInjected && !lastInjected) {
|
|
464
437
|
showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
|
|
465
438
|
}
|
|
466
439
|
return;
|
|
@@ -566,495 +539,6 @@ export function autoRecallHook(client, containerTags, tui, config = {}, getAgent
|
|
|
566
539
|
}
|
|
567
540
|
};
|
|
568
541
|
}
|
|
569
|
-
export 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
|
-
}
|
|
591
|
-
export function memoryInjectionHook(client, containerTags, tui, config = {}, getAgentName, directory) {
|
|
592
|
-
const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
|
|
593
|
-
const maxRecallResults = config.recall?.maxRecallResults ?? 10;
|
|
594
|
-
const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
|
|
595
|
-
const topkCapMultiplier = config.recall?.topkCapMultiplier ?? 2;
|
|
596
|
-
const mmrJaccardThreshold = config.recall?.mmrJaccardThreshold ?? 0.85;
|
|
597
|
-
const mmrPenaltyFactor = config.recall?.mmrPenaltyFactor ?? 0.5;
|
|
598
|
-
const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
|
|
599
|
-
const llmMaxEval = config.recall?.llmMaxEval ?? 15;
|
|
600
|
-
const refineStrategy = config.recall?.refineStrategy ?? "balanced";
|
|
601
|
-
const refineMediumChars = config.recall?.refineMediumChars ?? 200;
|
|
602
|
-
const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
|
|
603
|
-
const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
|
|
604
|
-
const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
|
|
605
|
-
return async (input, output) => {
|
|
606
|
-
if (!input.sessionID)
|
|
607
|
-
return;
|
|
608
|
-
const agentId = getAgentName?.() || process.env.OMEM_AGENT_ID || "opencode";
|
|
609
|
-
const policy = resolveAgentPolicy(agentId, config);
|
|
610
|
-
if (policy === "none")
|
|
611
|
-
return;
|
|
612
|
-
const isSaveKeyword = saveKeywordDetectedSessions.has(input.sessionID);
|
|
613
|
-
try {
|
|
614
|
-
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy, isSaveKeyword, similarityThreshold, maxRecallResults });
|
|
615
|
-
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
616
|
-
const userMessages = messages.filter((m) => m.role === "user");
|
|
617
|
-
if (userMessages.length === 0) {
|
|
618
|
-
logDebug("memoryInjectionHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
622
|
-
const query_text = extractUserRequest(rawQuery);
|
|
623
|
-
if (!query_text) {
|
|
624
|
-
logDebug("memoryInjectionHook filtered system injection", { rawQueryPrefix: rawQuery.slice(0, 60) });
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
628
|
-
const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
|
|
629
|
-
const conversationContext = userMessages.length >= 2
|
|
630
|
-
? userMessages.slice(-4, -1).map((m) => {
|
|
631
|
-
const stripped = stripPrivateContent(m.content);
|
|
632
|
-
return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
|
|
633
|
-
})
|
|
634
|
-
: undefined;
|
|
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 && cached.recallResult) {
|
|
643
|
-
isCacheHit = true;
|
|
644
|
-
shouldRecallRes = cached.recallResult;
|
|
645
|
-
if (cached.profileBlock) {
|
|
646
|
-
profileBlock = cached.profileBlock;
|
|
647
|
-
profileInjected = true;
|
|
648
|
-
profileCountText = cached.profileData?.countText ?? "";
|
|
649
|
-
}
|
|
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: "loose",
|
|
663
|
-
refine_medium_chars: refineMediumChars,
|
|
664
|
-
skip_llm_gate: true,
|
|
665
|
-
}, directory || process.env.OMEM_PROJECT_DIR),
|
|
666
|
-
]);
|
|
667
|
-
if (!recallRes) {
|
|
668
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API", "error", toastDelayMs);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
shouldRecallRes = recallRes;
|
|
672
|
-
// build profile block (with TTL check)
|
|
673
|
-
if (profile) {
|
|
674
|
-
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
675
|
-
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 10 * 60 * 1000);
|
|
676
|
-
if (ttlExpired) {
|
|
677
|
-
const built = buildProfileBlock(profile);
|
|
678
|
-
if (built) {
|
|
679
|
-
profileBlock = built.block;
|
|
680
|
-
profileCountText = built.countText;
|
|
681
|
-
profileInjected = true;
|
|
682
|
-
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
// write cache for next round
|
|
687
|
-
recallCache.set(input.sessionID, {
|
|
688
|
-
profileBlock,
|
|
689
|
-
recallResult: shouldRecallRes,
|
|
690
|
-
profileData: { countText: profileCountText },
|
|
691
|
-
timestamp: Date.now(),
|
|
692
|
-
});
|
|
693
|
-
// LRU eviction
|
|
694
|
-
if (recallCache.size > 50) {
|
|
695
|
-
let oldestKey = null;
|
|
696
|
-
let oldestTime = Infinity;
|
|
697
|
-
for (const [k, v] of recallCache) {
|
|
698
|
-
if (v.timestamp < oldestTime) {
|
|
699
|
-
oldestTime = v.timestamp;
|
|
700
|
-
oldestKey = k;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
if (oldestKey)
|
|
704
|
-
recallCache.delete(oldestKey);
|
|
705
|
-
}
|
|
706
|
-
// defensive check
|
|
707
|
-
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
708
|
-
logErr("memoryInjectionHook shouldRecall returned incomplete data", { shouldRecall: shouldRecallRes.should_recall, hasMemories: !!shouldRecallRes.memories });
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
logDebug("memoryInjectionHook cache miss, fetched synchronously", { sessionId: input.sessionID, shouldRecall: shouldRecallRes.should_recall, memCount: shouldRecallRes.memories?.length ?? 0 });
|
|
712
|
-
}
|
|
713
|
-
// ========== unified injection logic (cache hit + cache miss share this) ==========
|
|
714
|
-
if (!shouldRecallRes.should_recall) {
|
|
715
|
-
// no-recall path: inject profile only
|
|
716
|
-
const partsToInject = [];
|
|
717
|
-
if (profileBlock)
|
|
718
|
-
partsToInject.push(profileBlock);
|
|
719
|
-
if (partsToInject.length > 0) {
|
|
720
|
-
const injectText = partsToInject.join("\n\n");
|
|
721
|
-
const contextPart = {
|
|
722
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
723
|
-
sessionID: input.sessionID,
|
|
724
|
-
messageID: output.message.id,
|
|
725
|
-
type: "text",
|
|
726
|
-
text: injectText,
|
|
727
|
-
synthetic: true,
|
|
728
|
-
};
|
|
729
|
-
output.parts.unshift(contextPart);
|
|
730
|
-
logDebug("memoryInjectionHook profile injected (no-recall)", { sessionId: input.sessionID });
|
|
731
|
-
}
|
|
732
|
-
injectedSessions.add(input.sessionID);
|
|
733
|
-
const cacheTag = isCacheHit ? " (cached)" : "";
|
|
734
|
-
showToast(tui, `🧠 Profile Injected${cacheTag}`, profileCountText ? `Profile: ${profileCountText} · no recall needed` : "No memory recall needed", "success", toastDelayMs);
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
const results = shouldRecallRes.memories ?? [];
|
|
738
|
-
const clustered = shouldRecallRes.clustered;
|
|
739
|
-
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
|
|
740
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
741
|
-
logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
742
|
-
if (newResults.length === 0) {
|
|
743
|
-
const partsToInject = [];
|
|
744
|
-
if (profileBlock)
|
|
745
|
-
partsToInject.push(profileBlock);
|
|
746
|
-
if (partsToInject.length > 0) {
|
|
747
|
-
const injectText = partsToInject.join("\n\n");
|
|
748
|
-
const contextPart = {
|
|
749
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
750
|
-
sessionID: input.sessionID,
|
|
751
|
-
messageID: output.message.id,
|
|
752
|
-
type: "text",
|
|
753
|
-
text: injectText,
|
|
754
|
-
synthetic: true,
|
|
755
|
-
};
|
|
756
|
-
output.parts.unshift(contextPart);
|
|
757
|
-
logDebug("memoryInjectionHook profile injected (dedup)", { sessionId: input.sessionID });
|
|
758
|
-
}
|
|
759
|
-
injectedSessions.add(input.sessionID);
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
763
|
-
const budgetRemaining = maxContentChars - profileChars;
|
|
764
|
-
const itemCount = clustered
|
|
765
|
-
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
766
|
-
: newResults.length;
|
|
767
|
-
const dynamicMaxContentLength = itemCount > 0
|
|
768
|
-
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
769
|
-
: maxContentLength;
|
|
770
|
-
const block = clustered
|
|
771
|
-
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
772
|
-
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
773
|
-
const partsToInject = [];
|
|
774
|
-
if (block)
|
|
775
|
-
partsToInject.push(block);
|
|
776
|
-
if (block)
|
|
777
|
-
partsToInject.push(FETCH_POLICY);
|
|
778
|
-
if (profileBlock)
|
|
779
|
-
partsToInject.push(profileBlock);
|
|
780
|
-
if (isSaveKeyword)
|
|
781
|
-
partsToInject.push(KEYWORD_NUDGE);
|
|
782
|
-
if (partsToInject.length > 0) {
|
|
783
|
-
const injectText = partsToInject.join("\n\n");
|
|
784
|
-
const contextPart = {
|
|
785
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
786
|
-
sessionID: input.sessionID,
|
|
787
|
-
messageID: output.message.id,
|
|
788
|
-
type: "text",
|
|
789
|
-
text: injectText,
|
|
790
|
-
synthetic: true,
|
|
791
|
-
};
|
|
792
|
-
output.parts.unshift(contextPart);
|
|
793
|
-
logDebug("memoryInjectionHook block injected", {
|
|
794
|
-
sessionId: input.sessionID,
|
|
795
|
-
injectTextLen: injectText.length,
|
|
796
|
-
blockPreview: block?.slice(0, 200),
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
injectedSessions.add(input.sessionID);
|
|
800
|
-
if (isSaveKeyword) {
|
|
801
|
-
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
802
|
-
}
|
|
803
|
-
const newIds = newResults.map((r) => r.memory.id);
|
|
804
|
-
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
805
|
-
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
806
|
-
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
807
|
-
const memOther = newResults.length - memDynamic - memStatic;
|
|
808
|
-
let memCountMsg = "";
|
|
809
|
-
if (memDynamic > 0)
|
|
810
|
-
memCountMsg += `Dynamic(${memDynamic}) `;
|
|
811
|
-
if (memStatic > 0)
|
|
812
|
-
memCountMsg += `Static(${memStatic}) `;
|
|
813
|
-
if (memOther > 0)
|
|
814
|
-
memCountMsg += `Other(${memOther}) `;
|
|
815
|
-
const categories = categorize(newResults);
|
|
816
|
-
const catSummary = Array.from(categories.entries())
|
|
817
|
-
.map(([label, items]) => `${label}(${items.length})`)
|
|
818
|
-
.join(" · ");
|
|
819
|
-
let toastTitle;
|
|
820
|
-
let toastMessage;
|
|
821
|
-
if (clustered) {
|
|
822
|
-
const clusterCount = clustered.cluster_summaries.length;
|
|
823
|
-
const standaloneCount = clustered.standalone_memories.length;
|
|
824
|
-
toastTitle = `🧠 Context Injected${isCacheHit ? " (cached)" : ""} · ${clusterCount} clusters${standaloneCount > 0 ? ` · ${standaloneCount} standalone` : ""}`;
|
|
825
|
-
toastMessage = profileInjected
|
|
826
|
-
? `Profile: ${profileCountText} · Clustered memory display`
|
|
827
|
-
: `Clustered memory display`;
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
toastTitle = `🧠 Context Injected${isCacheHit ? " (cached)" : ""} · ${newResults.length} fragments`;
|
|
831
|
-
toastMessage = profileInjected
|
|
832
|
-
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
833
|
-
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
834
|
-
}
|
|
835
|
-
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
// cache miss: fire-and-forget createRecallEvent so web UI shows the record
|
|
839
|
-
if (!isCacheHit) {
|
|
840
|
-
if (shouldRecallRes.should_recall) {
|
|
841
|
-
const results = shouldRecallRes.memories ?? [];
|
|
842
|
-
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set();
|
|
843
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
844
|
-
const storedMemoryIds = results.map((r) => r.memory.id);
|
|
845
|
-
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
846
|
-
const maxScore = storedMemoryIds.length > 0
|
|
847
|
-
? Math.max(...(results.map((r) => r.score) ?? [0]))
|
|
848
|
-
: 0;
|
|
849
|
-
const bgBlock = shouldRecallRes.clustered
|
|
850
|
-
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
851
|
-
: buildContextBlock(newResults, maxContentLength);
|
|
852
|
-
const items = [
|
|
853
|
-
...(results.map((r) => ({
|
|
854
|
-
memory_id: r.memory.id, score: r.score,
|
|
855
|
-
refine_relevance: r.refine_relevance, refine_reasoning: r.refine_reasoning, is_kept: true,
|
|
856
|
-
}))),
|
|
857
|
-
...(shouldRecallRes.discarded?.map((d) => ({
|
|
858
|
-
memory_id: d.memory_id, score: d.score,
|
|
859
|
-
refine_relevance: d.refine_relevance, refine_reasoning: d.refine_reasoning, is_kept: false,
|
|
860
|
-
})) ?? []),
|
|
861
|
-
];
|
|
862
|
-
client.createRecallEvent({
|
|
863
|
-
session_id: input.sessionID, recall_type: "auto", query_text,
|
|
864
|
-
max_score: maxScore, llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
865
|
-
profile_injected: profileInjected,
|
|
866
|
-
kept_count: storedMemoryIds.length, discarded_count: storedDiscardedIds.length,
|
|
867
|
-
injected_count: newResults.length,
|
|
868
|
-
profile_content: profileInjected && profileBlock ? profileBlock : undefined,
|
|
869
|
-
injected_content: bgBlock ?? undefined,
|
|
870
|
-
items: items.length > 0 ? items : undefined,
|
|
871
|
-
}).catch((e) => {
|
|
872
|
-
logErr("memoryInjectionHook cache-miss createRecallEvent failed", { error: String(e) });
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
else if (profileInjected) {
|
|
876
|
-
client.createRecallEvent({
|
|
877
|
-
session_id: input.sessionID, recall_type: "auto", query_text,
|
|
878
|
-
max_score: 0, llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
879
|
-
profile_injected: true,
|
|
880
|
-
kept_count: 0, discarded_count: 0, injected_count: 0,
|
|
881
|
-
profile_content: profileBlock || undefined,
|
|
882
|
-
}).catch((e) => {
|
|
883
|
-
logErr("memoryInjectionHook cache-miss profile-only createRecallEvent failed", { error: String(e) });
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
logDebug("memoryInjectionHook injection complete", { sessionId: input.sessionID, isCacheHit });
|
|
888
|
-
// ========== Phase B: fire-and-forget async fetch for NEXT round (cache hit only) ==========
|
|
889
|
-
if (isCacheHit) {
|
|
890
|
-
const bgSessionId = input.sessionID;
|
|
891
|
-
const bgQueryText = query_text;
|
|
892
|
-
const bgLastQueryText = last_query_text;
|
|
893
|
-
const bgConversationContext = conversationContext;
|
|
894
|
-
const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
|
|
895
|
-
const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
|
|
896
|
-
Promise.allSettled([
|
|
897
|
-
client.getProfile(),
|
|
898
|
-
client.shouldRecall(bgQueryText, bgLastQueryText, bgSessionId, similarityThreshold, maxRecallResults, bgProjectTags, bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined, {
|
|
899
|
-
fetch_multiplier: fetchMultiplier,
|
|
900
|
-
topk_cap_multiplier: topkCapMultiplier,
|
|
901
|
-
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
902
|
-
mmr_penalty_factor: mmrPenaltyFactor,
|
|
903
|
-
phase2_multiplier: phase2Multiplier,
|
|
904
|
-
llm_max_eval: llmMaxEval,
|
|
905
|
-
refine_strategy: refineStrategy,
|
|
906
|
-
refine_medium_chars: refineMediumChars,
|
|
907
|
-
}, bgDirectory),
|
|
908
|
-
])
|
|
909
|
-
.then(([profileRes, recallRes]) => {
|
|
910
|
-
if (recallRes.status === 'rejected') {
|
|
911
|
-
logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
|
|
915
|
-
const shouldRecallRes = recallRes.value;
|
|
916
|
-
if (!shouldRecallRes) {
|
|
917
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
logDebug("memoryInjectionHook background fetch complete", {
|
|
921
|
-
sessionId: bgSessionId,
|
|
922
|
-
shouldRecall: shouldRecallRes.should_recall,
|
|
923
|
-
confidence: shouldRecallRes.confidence,
|
|
924
|
-
memCount: shouldRecallRes.memories?.length ?? 0,
|
|
925
|
-
});
|
|
926
|
-
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
927
|
-
logErr("memoryInjectionHook shouldRecall returned incomplete data", {
|
|
928
|
-
shouldRecall: shouldRecallRes.should_recall,
|
|
929
|
-
hasMemories: !!shouldRecallRes.memories,
|
|
930
|
-
});
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
let bgProfileBlock = "";
|
|
934
|
-
let bgProfileCountText = "";
|
|
935
|
-
let bgProfileInjected = false;
|
|
936
|
-
if (profile) {
|
|
937
|
-
const lastInjected = profileInjectedSessions.get(bgSessionId);
|
|
938
|
-
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 10 * 60 * 1000);
|
|
939
|
-
if (ttlExpired) {
|
|
940
|
-
const built = buildProfileBlock(profile);
|
|
941
|
-
if (built) {
|
|
942
|
-
bgProfileBlock = built.block;
|
|
943
|
-
bgProfileCountText = built.countText;
|
|
944
|
-
bgProfileInjected = true;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
recallCache.set(bgSessionId, {
|
|
949
|
-
profileBlock: bgProfileBlock,
|
|
950
|
-
recallResult: shouldRecallRes,
|
|
951
|
-
profileData: { countText: bgProfileCountText },
|
|
952
|
-
timestamp: Date.now(),
|
|
953
|
-
});
|
|
954
|
-
if (recallCache.size > 50) {
|
|
955
|
-
let oldestKey = null;
|
|
956
|
-
let oldestTime = Infinity;
|
|
957
|
-
for (const [k, v] of recallCache) {
|
|
958
|
-
if (v.timestamp < oldestTime) {
|
|
959
|
-
oldestTime = v.timestamp;
|
|
960
|
-
oldestKey = k;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (oldestKey)
|
|
964
|
-
recallCache.delete(oldestKey);
|
|
965
|
-
}
|
|
966
|
-
if (shouldRecallRes.should_recall) {
|
|
967
|
-
const results = shouldRecallRes.memories ?? [];
|
|
968
|
-
const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set();
|
|
969
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
970
|
-
if (newResults.length > 0) {
|
|
971
|
-
const newIds = newResults.map((r) => r.memory.id);
|
|
972
|
-
injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
|
|
973
|
-
}
|
|
974
|
-
const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
|
|
975
|
-
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
976
|
-
const maxScore = storedMemoryIds.length > 0
|
|
977
|
-
? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
|
|
978
|
-
: 0;
|
|
979
|
-
const bgBlock = shouldRecallRes.clustered
|
|
980
|
-
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
981
|
-
: buildContextBlock(newResults, maxContentLength);
|
|
982
|
-
const bgInjectedContent = bgBlock ?? undefined;
|
|
983
|
-
const items = [
|
|
984
|
-
...(shouldRecallRes.memories?.map((r) => ({
|
|
985
|
-
memory_id: r.memory.id,
|
|
986
|
-
score: r.score,
|
|
987
|
-
refine_relevance: r.refine_relevance,
|
|
988
|
-
refine_reasoning: r.refine_reasoning,
|
|
989
|
-
is_kept: true,
|
|
990
|
-
})) ?? []),
|
|
991
|
-
...(shouldRecallRes.discarded?.map((d) => ({
|
|
992
|
-
memory_id: d.memory_id,
|
|
993
|
-
score: d.score,
|
|
994
|
-
refine_relevance: d.refine_relevance,
|
|
995
|
-
refine_reasoning: d.refine_reasoning,
|
|
996
|
-
is_kept: false,
|
|
997
|
-
})) ?? []),
|
|
998
|
-
];
|
|
999
|
-
client.createRecallEvent({
|
|
1000
|
-
session_id: bgSessionId,
|
|
1001
|
-
recall_type: "auto",
|
|
1002
|
-
query_text: bgQueryText,
|
|
1003
|
-
max_score: maxScore,
|
|
1004
|
-
llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
1005
|
-
profile_injected: bgProfileInjected,
|
|
1006
|
-
kept_count: storedMemoryIds.length,
|
|
1007
|
-
discarded_count: storedDiscardedIds.length,
|
|
1008
|
-
injected_count: newResults.length,
|
|
1009
|
-
profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
|
|
1010
|
-
injected_content: bgInjectedContent,
|
|
1011
|
-
items: items.length > 0 ? items : undefined,
|
|
1012
|
-
}).catch((e) => {
|
|
1013
|
-
logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
})
|
|
1017
|
-
.catch((err) => {
|
|
1018
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1019
|
-
logErr("memoryInjectionHook background fetch failed", { error: errMsg });
|
|
1020
|
-
if (errMsg.includes("[cerebro]")) {
|
|
1021
|
-
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
1022
|
-
if (cleanMsg.startsWith("500")) {
|
|
1023
|
-
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
1024
|
-
}
|
|
1025
|
-
else if (cleanMsg.includes("timed out")) {
|
|
1026
|
-
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
1030
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
catch (err) {
|
|
1036
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1037
|
-
if (errMsg.includes("[cerebro]")) {
|
|
1038
|
-
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
1039
|
-
if (cleanMsg.startsWith("500")) {
|
|
1040
|
-
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
1041
|
-
}
|
|
1042
|
-
else if (cleanMsg.includes("timed out")) {
|
|
1043
|
-
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
1044
|
-
}
|
|
1045
|
-
else {
|
|
1046
|
-
showToast(tui, "🧠 Cerebro Error", cleanMsg.substring(0, 150), "error");
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
1050
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
1051
|
-
}
|
|
1052
|
-
else {
|
|
1053
|
-
showToast(tui, "🧠 Memory Recall Error", errMsg.substring(0, 100), "error");
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
542
|
export function keywordDetectionHook(_client, _containerTags, threshold, _tui, _ingestMode = "smart", config = {}, agentId) {
|
|
1059
543
|
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
1060
544
|
return async (input, output) => {
|
|
@@ -1180,7 +664,6 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
|
|
|
1180
664
|
if (input.sessionID) {
|
|
1181
665
|
sessionMessages.delete(input.sessionID);
|
|
1182
666
|
profileInjectedSessions.delete(input.sessionID);
|
|
1183
|
-
recallCache.delete(input.sessionID);
|
|
1184
667
|
firstMessages.delete(input.sessionID);
|
|
1185
668
|
}
|
|
1186
669
|
return;
|
|
@@ -1210,7 +693,6 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
|
|
|
1210
693
|
if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
|
|
1211
694
|
sessionMessages.delete(input.sessionID);
|
|
1212
695
|
profileInjectedSessions.delete(input.sessionID);
|
|
1213
|
-
recallCache.delete(input.sessionID);
|
|
1214
696
|
firstMessages.delete(input.sessionID);
|
|
1215
697
|
}
|
|
1216
698
|
else {
|
|
@@ -1242,160 +724,20 @@ export function compactingHook(client, containerTags, tui, ingestMode = "smart",
|
|
|
1242
724
|
}
|
|
1243
725
|
// Cleanup tracked messages regardless of ingest result
|
|
1244
726
|
sessionMessages.delete(input.sessionID);
|
|
1245
|
-
injectedSessions.delete(input.sessionID);
|
|
1246
727
|
profileInjectedSessions.delete(input.sessionID);
|
|
1247
|
-
recallCache.delete(input.sessionID);
|
|
1248
728
|
firstMessages.delete(input.sessionID);
|
|
1249
729
|
if (input.sessionID) {
|
|
1250
|
-
|
|
1251
|
-
logDebug("compactingHook cleared session pendingToolCalls", { sessionID: input.sessionID, hadPending: deleted });
|
|
730
|
+
logDebug("compactingHook cleared session state", { sessionID: input.sessionID });
|
|
1252
731
|
}
|
|
1253
732
|
// Evict stale injectedMemoryIds if over size cap (200 sessions)
|
|
1254
733
|
if (injectedMemoryIds.size > 200) {
|
|
1255
734
|
injectedMemoryIds.clear();
|
|
1256
735
|
}
|
|
1257
736
|
}
|
|
1258
|
-
//
|
|
1259
|
-
if (
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
const pollProjectName = projectName;
|
|
1263
|
-
const pollProjectPath = projectPath;
|
|
1264
|
-
const pollAgentId = effectiveAgentId;
|
|
1265
|
-
let baselineMsgIds = new Set();
|
|
1266
|
-
try {
|
|
1267
|
-
const preResp = await sdkClient.session.messages({ path: { id: pollSessionId } });
|
|
1268
|
-
if (preResp?.data) {
|
|
1269
|
-
baselineMsgIds = new Set(preResp.data.map((m) => m.info?.id).filter(Boolean));
|
|
1270
|
-
}
|
|
1271
|
-
logInfo("compactingHook: summary poll starting", { baselineCount: baselineMsgIds.size, sessionId: pollSessionId });
|
|
1272
|
-
}
|
|
1273
|
-
catch (e) {
|
|
1274
|
-
logErr("compactingHook: baseline snapshot failed", { error: String(e) });
|
|
1275
|
-
}
|
|
1276
|
-
if (baselineMsgIds.size > 0) {
|
|
1277
|
-
const maxAttempts = 12;
|
|
1278
|
-
const pollInterval = 5000;
|
|
1279
|
-
const COMPACT_MARKER = "[restore checkpointed";
|
|
1280
|
-
(async () => {
|
|
1281
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
1282
|
-
await new Promise(r => setTimeout(r, pollInterval));
|
|
1283
|
-
try {
|
|
1284
|
-
const resp = await sdkClient.session.messages({ path: { id: pollSessionId } });
|
|
1285
|
-
if (!resp?.data)
|
|
1286
|
-
continue;
|
|
1287
|
-
const currentCount = resp.data.length;
|
|
1288
|
-
logDebug("compactingHook: summary poll tick", {
|
|
1289
|
-
attempt: i + 1, currentCount, baselineCount: baselineMsgIds.size,
|
|
1290
|
-
});
|
|
1291
|
-
const compactMsg = resp.data.find((m) => {
|
|
1292
|
-
if (m.info?.role !== "user")
|
|
1293
|
-
return false;
|
|
1294
|
-
if (baselineMsgIds.has(m.info?.id))
|
|
1295
|
-
return false;
|
|
1296
|
-
const textParts = (m.parts || [])
|
|
1297
|
-
.filter((p) => p.type === "text" && p.text)
|
|
1298
|
-
.map((p) => p.text);
|
|
1299
|
-
return textParts.join("\n").includes(COMPACT_MARKER);
|
|
1300
|
-
});
|
|
1301
|
-
if (compactMsg) {
|
|
1302
|
-
const compactIdx = resp.data.findIndex((m) => m.info?.id === compactMsg.info?.id);
|
|
1303
|
-
const userTextParts = (compactMsg.parts || [])
|
|
1304
|
-
.filter((p) => p.type === "text" && p.text)
|
|
1305
|
-
.map((p) => p.text);
|
|
1306
|
-
const userFullText = userTextParts.join("\n").trim();
|
|
1307
|
-
logInfo("compactingHook: compact completed detected", {
|
|
1308
|
-
attempt: i + 1, msgId: compactMsg.info?.id,
|
|
1309
|
-
compactIdx, userTextLen: userFullText.length,
|
|
1310
|
-
partsCount: (compactMsg.parts || []).length,
|
|
1311
|
-
partTypes: (compactMsg.parts || []).map((p) => p.type),
|
|
1312
|
-
firstPartLen: userTextParts[0]?.length ?? 0,
|
|
1313
|
-
msgsAfterCompact: resp.data.length - compactIdx - 1,
|
|
1314
|
-
});
|
|
1315
|
-
if (userFullText.length > 0) {
|
|
1316
|
-
logDebug("compactingHook: compact msg full text", {
|
|
1317
|
-
text: userFullText.substring(0, 500),
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
let summaryText;
|
|
1321
|
-
const markerLineIdx = userFullText.indexOf(COMPACT_MARKER);
|
|
1322
|
-
if (markerLineIdx >= 0) {
|
|
1323
|
-
const afterMarker = userFullText.substring(markerLineIdx);
|
|
1324
|
-
const firstNewline = afterMarker.indexOf("\n");
|
|
1325
|
-
const candidate = firstNewline >= 0 ? afterMarker.substring(firstNewline + 1).trim() : "";
|
|
1326
|
-
if (candidate.length > 100) {
|
|
1327
|
-
summaryText = candidate;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
if (!summaryText && compactIdx >= 0) {
|
|
1331
|
-
for (let j = compactIdx + 1; j < resp.data.length; j++) {
|
|
1332
|
-
const msg = resp.data[j];
|
|
1333
|
-
if (msg.info?.role !== "assistant")
|
|
1334
|
-
continue;
|
|
1335
|
-
const assistParts = (msg.parts || [])
|
|
1336
|
-
.filter((p) => p.type === "text" && p.text)
|
|
1337
|
-
.map((p) => p.text);
|
|
1338
|
-
const assistText = assistParts.join("\n").trim();
|
|
1339
|
-
logDebug("compactingHook: assistant msg after compact", {
|
|
1340
|
-
idx: j, textLen: assistText.length, partTypes: (msg.parts || []).map((p) => p.type),
|
|
1341
|
-
preview: assistText.substring(0, 200),
|
|
1342
|
-
});
|
|
1343
|
-
if (assistText.length > 200) {
|
|
1344
|
-
summaryText = assistText;
|
|
1345
|
-
break;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
if (!summaryText && userFullText.length > 100) {
|
|
1350
|
-
summaryText = userFullText;
|
|
1351
|
-
}
|
|
1352
|
-
if (summaryText) {
|
|
1353
|
-
logInfo("compactingHook: storing compact summary", {
|
|
1354
|
-
summaryLen: summaryText.length, msgId: compactMsg.info?.id,
|
|
1355
|
-
});
|
|
1356
|
-
// Dedup check: 30s cooldown per session+content hash
|
|
1357
|
-
const summaryHash = `${pollSessionId}:${hashString(summaryText)}`;
|
|
1358
|
-
const lastCompacting = compactingSummaryCooldown.get(summaryHash);
|
|
1359
|
-
if (lastCompacting && Date.now() - lastCompacting < 30000) {
|
|
1360
|
-
logDebug("compactingHook summary dedup", { sessionId: pollSessionId });
|
|
1361
|
-
break;
|
|
1362
|
-
}
|
|
1363
|
-
compactingSummaryCooldown.set(summaryHash, Date.now());
|
|
1364
|
-
const prefixedSummary = `[Session Summary] ${summaryText}`;
|
|
1365
|
-
try {
|
|
1366
|
-
const result = await client.ingestMessages([{ role: "user", content: prefixedSummary }], {
|
|
1367
|
-
mode: ingestMode,
|
|
1368
|
-
tags: [...containerTags, "auto-capture", "compact-summary"],
|
|
1369
|
-
sessionId: pollEffectiveSessionId,
|
|
1370
|
-
projectName: pollProjectName,
|
|
1371
|
-
agentId: pollAgentId,
|
|
1372
|
-
projectPath: pollProjectPath,
|
|
1373
|
-
});
|
|
1374
|
-
logInfo("compactingHook: compact summary store result", {
|
|
1375
|
-
result: result === null ? "null(blocked)" : "ok",
|
|
1376
|
-
});
|
|
1377
|
-
if (result !== null) {
|
|
1378
|
-
showToast(tui, "📦 Compact Summary Stored", "Session summary archived to memory", "success");
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
catch (e) {
|
|
1382
|
-
logErr("compactingHook: compact summary store failed", { error: String(e) });
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
else {
|
|
1386
|
-
logInfo("compactingHook: no summary text found after compact marker", {
|
|
1387
|
-
userTextLen: userFullText.length, compactIdx,
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
break;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
catch (e) {
|
|
1394
|
-
logErr("compactingHook: summary poll error", { error: String(e), attempt: i + 1 });
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
})();
|
|
1398
|
-
}
|
|
737
|
+
// After compacting, clear profile TTL so next autoRecallHook re-injects profile
|
|
738
|
+
if (input.sessionID) {
|
|
739
|
+
profileInjectedSessions.delete(input.sessionID);
|
|
740
|
+
logDebug("compactingHook cleared profile TTL for re-injection", { sessionID: input.sessionID });
|
|
1399
741
|
}
|
|
1400
742
|
};
|
|
1401
743
|
}
|
|
@@ -1483,54 +825,106 @@ export function autocontinueHook(client, containerTags, tui, ingestMode = "smart
|
|
|
1483
825
|
}
|
|
1484
826
|
const processedMessageIds = new Set();
|
|
1485
827
|
const pluginStartTime = Date.now();
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
828
|
+
export function sessionIdleHook(cerebroClient, containerTags, tui, sdkClient, ingestMode = "smart", threshold = 0, getMainSessionId, isAutoStoreEnabled, agentId, config = {}, onAgentResolved, directory) {
|
|
829
|
+
let idleTimeout = null;
|
|
830
|
+
let isCapturing = false;
|
|
831
|
+
async function handleSummaryCapture(props) {
|
|
832
|
+
const info = props?.info;
|
|
833
|
+
if (!info)
|
|
834
|
+
return;
|
|
835
|
+
if (info.role !== "assistant" || !info.summary || !info.finish)
|
|
836
|
+
return;
|
|
837
|
+
const sessionID = info.sessionID;
|
|
838
|
+
if (!sessionID)
|
|
839
|
+
return;
|
|
840
|
+
if (summarizedSessions.has(sessionID))
|
|
841
|
+
return;
|
|
842
|
+
summarizedSessions.add(sessionID);
|
|
843
|
+
if (!sdkClient) {
|
|
844
|
+
logInfo("handleSummaryCapture skipped: no sdkClient", { sessionID });
|
|
1492
845
|
return;
|
|
1493
846
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
847
|
+
logInfo("handleSummaryCapture triggered", { sessionID });
|
|
848
|
+
if (getMainSessionId) {
|
|
849
|
+
const mainId = getMainSessionId();
|
|
850
|
+
if (mainId && sessionID !== mainId) {
|
|
851
|
+
logInfo("handleSummaryCapture: non-main session skipped", { sessionID, mainSessionId: mainId });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
856
|
+
const policy = resolveAgentPolicy(effectiveAgentId, config);
|
|
857
|
+
if (policy !== "readwrite") {
|
|
858
|
+
logInfo("handleSummaryCapture blocked by policy", { agentId: effectiveAgentId, policy });
|
|
1499
859
|
return;
|
|
1500
860
|
}
|
|
1501
|
-
|
|
1502
|
-
const isWildcard = includeTools.includes("*");
|
|
1503
|
-
if (!isWildcard && !includeTools.includes(toolName)) {
|
|
1504
|
-
logDebug("soulWhisperToolTracker not in whitelist", { tool: toolName, whitelist: includeTools });
|
|
861
|
+
if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID))
|
|
1505
862
|
return;
|
|
863
|
+
try {
|
|
864
|
+
const resp = await sdkClient.session.messages({ path: { id: sessionID } });
|
|
865
|
+
const messages = resp?.data ?? resp;
|
|
866
|
+
const summaryMsg = messages.find((m) => m.info?.role === "assistant" && m.info?.summary === true);
|
|
867
|
+
if (!summaryMsg?.parts) {
|
|
868
|
+
logInfo("handleSummaryCapture: no summary parts found", { sessionID });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const textParts = summaryMsg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text);
|
|
872
|
+
const summaryContent = textParts.join("\n").trim();
|
|
873
|
+
if (!summaryContent || summaryContent.length < 100) {
|
|
874
|
+
logInfo("handleSummaryCapture: summary too short", { sessionID, length: summaryContent?.length ?? 0 });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const effectiveSessionId = getMainSessionId?.() || sessionID;
|
|
878
|
+
let projectName;
|
|
879
|
+
let projectPath;
|
|
880
|
+
try {
|
|
881
|
+
const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
|
|
882
|
+
projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
|
|
883
|
+
projectName = sessionInfo?.data?.directory
|
|
884
|
+
? await detectProjectName(sessionInfo.data.directory)
|
|
885
|
+
: undefined;
|
|
886
|
+
}
|
|
887
|
+
catch (e) {
|
|
888
|
+
logErr("handleSummaryCapture detectProjectName failed", { error: String(e) });
|
|
889
|
+
}
|
|
890
|
+
if (!projectPath) {
|
|
891
|
+
projectPath = directory || process.env.OMEM_PROJECT_DIR;
|
|
892
|
+
}
|
|
893
|
+
const prefixedSummary = `[Session Summary] ${summaryContent}`;
|
|
894
|
+
const result = await cerebroClient.ingestMessages([{ role: "user", content: prefixedSummary }], {
|
|
895
|
+
mode: ingestMode,
|
|
896
|
+
tags: [...containerTags, "auto-capture", "compact-summary"],
|
|
897
|
+
sessionId: effectiveSessionId,
|
|
898
|
+
projectName,
|
|
899
|
+
agentId: effectiveAgentId,
|
|
900
|
+
projectPath,
|
|
901
|
+
});
|
|
902
|
+
logInfo("handleSummaryCapture store result", { result: result === null ? "null(blocked)" : "ok" });
|
|
903
|
+
if (result !== null) {
|
|
904
|
+
showToast(tui, "📦 Compact Summary Stored", "Session summary archived", "success");
|
|
905
|
+
}
|
|
1506
906
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
if (!sessionMap) {
|
|
1510
|
-
sessionMap = new Map();
|
|
1511
|
-
pendingToolCalls.set(sid, sessionMap);
|
|
907
|
+
catch (err) {
|
|
908
|
+
logErr("handleSummaryCapture failed", { error: String(err) });
|
|
1512
909
|
}
|
|
1513
|
-
sessionMap.set(input.callID, { toolName, timestamp: Date.now() });
|
|
1514
|
-
logDebug("soulWhisperToolTracker recorded", { tool: toolName, callID: input.callID, sessionID: sid, totalSessions: pendingToolCalls.size, sessionCallCount: sessionMap.size });
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
export function buildWhisperText(toolNames, maxToolNames) {
|
|
1518
|
-
if (toolNames.length === 0)
|
|
1519
|
-
return null;
|
|
1520
|
-
const lines = ["<cerebro-memory-activation>"];
|
|
1521
|
-
if (toolNames.length <= maxToolNames) {
|
|
1522
|
-
lines.push(`Before using ${toolNames.join(", ")}, memory_search() may surface relevant past decisions or patterns. Brief recall → better outcomes.`);
|
|
1523
|
-
}
|
|
1524
|
-
else {
|
|
1525
|
-
lines.push("Before you act — memory_search() surfaces cross-session context: past decisions, user preferences, hard-won insights. The strongest responses are built on remembered context.");
|
|
1526
910
|
}
|
|
1527
|
-
lines.push("</cerebro-memory-activation>");
|
|
1528
|
-
return lines.join("\n");
|
|
1529
|
-
}
|
|
1530
|
-
export function sessionIdleHook(cerebroClient, _containerTags, tui, sdkClient, _ingestMode = "smart", threshold = 0, getMainSessionId, isAutoStoreEnabled, agentId, config = {}, onAgentResolved, directory) {
|
|
1531
|
-
let idleTimeout = null;
|
|
1532
|
-
let isCapturing = false;
|
|
1533
911
|
return async (input) => {
|
|
912
|
+
if (input.event.type === "message.updated") {
|
|
913
|
+
await handleSummaryCapture(input.event.properties);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (input.event.type === "session.deleted") {
|
|
917
|
+
const sessionInfo = input.event.properties?.info;
|
|
918
|
+
const sid = sessionInfo?.id;
|
|
919
|
+
if (sid) {
|
|
920
|
+
summarizedSessions.delete(sid);
|
|
921
|
+
sessionMessages.delete(sid);
|
|
922
|
+
profileInjectedSessions.delete(sid);
|
|
923
|
+
firstMessages.delete(sid);
|
|
924
|
+
logDebug("sessionIdleHook: session.deleted cleanup", { sessionID: sid });
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
1534
928
|
if (input.event.type !== "session.idle")
|
|
1535
929
|
return;
|
|
1536
930
|
logDebug("sessionIdleHook event.properties dump", { keys: Object.keys(input.event.properties || {}), raw: JSON.stringify(input.event.properties).substring(0, 2000) });
|
|
@@ -1634,9 +1028,6 @@ export function sessionIdleHook(cerebroClient, _containerTags, tui, sdkClient, _
|
|
|
1634
1028
|
finally {
|
|
1635
1029
|
isCapturing = false;
|
|
1636
1030
|
idleTimeout = null;
|
|
1637
|
-
const deleted = pendingToolCalls.delete(sessionID);
|
|
1638
|
-
if (deleted)
|
|
1639
|
-
logDebug("sessionIdleHook cleared session pendingToolCalls", { sessionID, hadPending: deleted });
|
|
1640
1031
|
}
|
|
1641
1032
|
}, 10000);
|
|
1642
1033
|
};
|