@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/src/hooks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Model, UserMessage, Part } from "@opencode-ai/sdk";
|
|
2
|
-
import type { CerebroClient, SearchResult
|
|
2
|
+
import type { CerebroClient, SearchResult } from "./client.js";
|
|
3
3
|
import { type OmemPluginConfig, resolveAgentPolicy } from "./config.js";
|
|
4
4
|
import { detectSaveKeyword, KEYWORD_NUDGE } from "./keywords.js";
|
|
5
5
|
import { logDebug, logInfo, logError as logErr } from "./logger.js";
|
|
@@ -124,7 +124,7 @@ async function detectProjectName(rootPath: string): Promise<string | undefined>
|
|
|
124
124
|
return result;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function showToast(tui: any, title: string, message: string, variant: string = "info", delayMs: number = 7000) {
|
|
127
|
+
export function showToast(tui: any, title: string, message: string, variant: string = "info", delayMs: number = 7000) {
|
|
128
128
|
if (!tui) return;
|
|
129
129
|
setTimeout(() => {
|
|
130
130
|
try {
|
|
@@ -172,26 +172,7 @@ const injectedMemoryIds = new Map<string, Set<string>>();
|
|
|
172
172
|
const firstMessages = new Map<string, string>();
|
|
173
173
|
const sessionMessages = new Map<string, Array<{ role: string; content: string }>>();
|
|
174
174
|
export const profileInjectedSessions = new Map<string, number>();
|
|
175
|
-
const
|
|
176
|
-
const compactingSummaryCooldown = new Map<string, number>();
|
|
177
|
-
|
|
178
|
-
// Per-session async cache for fire-and-forget recall results
|
|
179
|
-
export const recallCache = new Map<string, {
|
|
180
|
-
profileBlock: string;
|
|
181
|
-
recallResult: ShouldRecallResponse;
|
|
182
|
-
profileData: { countText: string };
|
|
183
|
-
timestamp: number;
|
|
184
|
-
}>();
|
|
185
|
-
|
|
186
|
-
function hashString(str: string): string {
|
|
187
|
-
let hash = 0;
|
|
188
|
-
for (let i = 0; i < str.length; i++) {
|
|
189
|
-
const char = str.charCodeAt(i);
|
|
190
|
-
hash = ((hash << 5) - hash) + char;
|
|
191
|
-
hash |= 0;
|
|
192
|
-
}
|
|
193
|
-
return hash.toString(36);
|
|
194
|
-
}
|
|
175
|
+
const summarizedSessions = new Set<string>();
|
|
195
176
|
|
|
196
177
|
function formatRelativeAge(isoDate: string): string {
|
|
197
178
|
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
@@ -356,40 +337,28 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
356
337
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
357
338
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
358
339
|
|
|
359
|
-
// --- Profile Fetch (
|
|
360
|
-
const
|
|
340
|
+
// --- Profile Fetch (V2 inject API with TTL gate) ---
|
|
341
|
+
const profileTtlMs = config.profile?.ttlMs ?? 300000; // default 5 minutes
|
|
342
|
+
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
343
|
+
const profileTtlExpired = !lastInjected || (Date.now() - lastInjected > profileTtlMs);
|
|
344
|
+
|
|
345
|
+
let profileBlock = "";
|
|
361
346
|
let profileInjected = false;
|
|
362
347
|
let profileCountText = "";
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
: " · (preferences queuing, will populate on next refresh)";
|
|
378
|
-
profileBlock = [
|
|
379
|
-
"<cerebro-profile>",
|
|
380
|
-
profileLines,
|
|
381
|
-
"</cerebro-profile>",
|
|
382
|
-
].join("\n");
|
|
383
|
-
profileInjected = true;
|
|
384
|
-
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
385
|
-
const p = profile as any;
|
|
386
|
-
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
387
|
-
const staticCount = p?.static_facts?.length ?? 0;
|
|
388
|
-
profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
389
|
-
if (isFirstInjection) {
|
|
390
|
-
logDebug("autoRecallHook profile ready (first)", { dynamicCount, staticCount });
|
|
391
|
-
} else {
|
|
392
|
-
logDebug("autoRecallHook profile ready (TTL)", { dynamicCount, staticCount });
|
|
348
|
+
|
|
349
|
+
if (profileTtlExpired) {
|
|
350
|
+
try {
|
|
351
|
+
const injection = await client.getInjection(directory || process.env.OMEM_PROJECT_DIR);
|
|
352
|
+
if (injection?.content) {
|
|
353
|
+
profileBlock = injection.content;
|
|
354
|
+
profileCountText = `${injection.preference_count} preferences`;
|
|
355
|
+
profileInjected = true;
|
|
356
|
+
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
357
|
+
logDebug("autoRecallHook profile ready (V2 injection)", { preferenceCount: injection.preference_count, estimatedTokens: injection.estimated_tokens });
|
|
358
|
+
}
|
|
359
|
+
} catch (e) {
|
|
360
|
+
logErr("autoRecallHook getInjection failed, skipping profile", { error: String(e) });
|
|
361
|
+
// profile failure does not block shouldRecall
|
|
393
362
|
}
|
|
394
363
|
}
|
|
395
364
|
|
|
@@ -496,8 +465,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
496
465
|
appendToSystem(output.system, profileBlock);
|
|
497
466
|
logDebug("autoRecallHook profile injected (no-recall path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
|
|
498
467
|
}
|
|
499
|
-
if (profileInjected &&
|
|
500
|
-
await createEventAndReturn(0, 0, 0);
|
|
468
|
+
if (profileInjected && !lastInjected) {
|
|
501
469
|
showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
|
|
502
470
|
}
|
|
503
471
|
return;
|
|
@@ -514,7 +482,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
514
482
|
appendToSystem(output.system, profileBlock);
|
|
515
483
|
logDebug("autoRecallHook profile injected (dedup path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
|
|
516
484
|
}
|
|
517
|
-
if (profileInjected &&
|
|
485
|
+
if (profileInjected && !lastInjected) {
|
|
518
486
|
showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
|
|
519
487
|
}
|
|
520
488
|
return;
|
|
@@ -623,537 +591,6 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
623
591
|
};
|
|
624
592
|
}
|
|
625
593
|
|
|
626
|
-
export function buildProfileBlock(profile: any): { block: string; countText: string } | null {
|
|
627
|
-
const prefs = ((profile as any)?.static_facts ?? [])
|
|
628
|
-
.filter((sf: any) => {
|
|
629
|
-
const t: string[] = sf.tags ?? [];
|
|
630
|
-
return t.includes("preferences");
|
|
631
|
-
})
|
|
632
|
-
.map((sf: any) => sf.l2_content ?? sf.content ?? "")
|
|
633
|
-
.filter(Boolean);
|
|
634
|
-
const profileLines = prefs.length > 0
|
|
635
|
-
? prefs.map((c: string) => ` · ${c}`).join("\n")
|
|
636
|
-
: " · (preferences queuing, will populate on next refresh)";
|
|
637
|
-
const block = [
|
|
638
|
-
"<cerebro-profile>",
|
|
639
|
-
profileLines,
|
|
640
|
-
"</cerebro-profile>",
|
|
641
|
-
].join("\n");
|
|
642
|
-
const p = profile as any;
|
|
643
|
-
const dynamicCount = p?.dynamic_context?.length ?? 0;
|
|
644
|
-
const staticCount = p?.static_facts?.length ?? 0;
|
|
645
|
-
const countText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
|
|
646
|
-
return { block, countText };
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
export function memoryInjectionHook(
|
|
650
|
-
client: CerebroClient,
|
|
651
|
-
containerTags: string[],
|
|
652
|
-
tui: any,
|
|
653
|
-
config: Partial<OmemPluginConfig> = {},
|
|
654
|
-
getAgentName?: () => string,
|
|
655
|
-
directory?: string,
|
|
656
|
-
) {
|
|
657
|
-
const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
|
|
658
|
-
const maxRecallResults = config.recall?.maxRecallResults ?? 10;
|
|
659
|
-
const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
|
|
660
|
-
const topkCapMultiplier = config.recall?.topkCapMultiplier ?? 2;
|
|
661
|
-
const mmrJaccardThreshold = config.recall?.mmrJaccardThreshold ?? 0.85;
|
|
662
|
-
const mmrPenaltyFactor = config.recall?.mmrPenaltyFactor ?? 0.5;
|
|
663
|
-
const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
|
|
664
|
-
const llmMaxEval = config.recall?.llmMaxEval ?? 15;
|
|
665
|
-
const refineStrategy = config.recall?.refineStrategy ?? "balanced";
|
|
666
|
-
const refineMediumChars = config.recall?.refineMediumChars ?? 200;
|
|
667
|
-
const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
|
|
668
|
-
const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
|
|
669
|
-
const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
|
|
670
|
-
|
|
671
|
-
return async (
|
|
672
|
-
input: { sessionID?: string; messageID?: string; model: Model },
|
|
673
|
-
output: { message: UserMessage; parts: Part[] },
|
|
674
|
-
) => {
|
|
675
|
-
if (!input.sessionID) return;
|
|
676
|
-
|
|
677
|
-
const agentId = getAgentName?.() || process.env.OMEM_AGENT_ID || "opencode";
|
|
678
|
-
const policy = resolveAgentPolicy(agentId, config);
|
|
679
|
-
if (policy === "none") return;
|
|
680
|
-
|
|
681
|
-
const isSaveKeyword = saveKeywordDetectedSessions.has(input.sessionID);
|
|
682
|
-
|
|
683
|
-
try {
|
|
684
|
-
logDebug("memoryInjectionHook start", { sessionId: input.sessionID, agentId, policy, isSaveKeyword, similarityThreshold, maxRecallResults });
|
|
685
|
-
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
686
|
-
const userMessages = messages.filter((m) => m.role === "user");
|
|
687
|
-
|
|
688
|
-
if (userMessages.length === 0) {
|
|
689
|
-
logDebug("memoryInjectionHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
694
|
-
const query_text = extractUserRequest(rawQuery);
|
|
695
|
-
if (!query_text) {
|
|
696
|
-
logDebug("memoryInjectionHook filtered system injection", { rawQueryPrefix: rawQuery.slice(0, 60) });
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
700
|
-
|
|
701
|
-
const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
|
|
702
|
-
|
|
703
|
-
const conversationContext = userMessages.length >= 2
|
|
704
|
-
? userMessages.slice(-4, -1).map((m) => {
|
|
705
|
-
const stripped = stripPrivateContent(m.content);
|
|
706
|
-
return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
|
|
707
|
-
})
|
|
708
|
-
: undefined;
|
|
709
|
-
|
|
710
|
-
// ========== Phase A: unified data fetch + injection ==========
|
|
711
|
-
let shouldRecallRes: ShouldRecallResponse;
|
|
712
|
-
let profileBlock = "";
|
|
713
|
-
let profileInjected = false;
|
|
714
|
-
let profileCountText = "";
|
|
715
|
-
let isCacheHit = false;
|
|
716
|
-
|
|
717
|
-
const cached = recallCache.get(input.sessionID);
|
|
718
|
-
|
|
719
|
-
if (cached && cached.recallResult) {
|
|
720
|
-
isCacheHit = true;
|
|
721
|
-
shouldRecallRes = cached.recallResult;
|
|
722
|
-
if (cached.profileBlock) {
|
|
723
|
-
profileBlock = cached.profileBlock;
|
|
724
|
-
profileInjected = true;
|
|
725
|
-
profileCountText = cached.profileData?.countText ?? "";
|
|
726
|
-
}
|
|
727
|
-
} else {
|
|
728
|
-
// cache miss: synchronous await (first message takes 5-8s, but gets injection)
|
|
729
|
-
const [profile, recallRes] = await Promise.all([
|
|
730
|
-
client.getProfile(),
|
|
731
|
-
client.shouldRecall(
|
|
732
|
-
query_text, last_query_text, input.sessionID,
|
|
733
|
-
similarityThreshold, maxRecallResults,
|
|
734
|
-
projectTags.length > 0 ? projectTags : undefined,
|
|
735
|
-
conversationContext && conversationContext.length > 0 ? conversationContext : undefined,
|
|
736
|
-
{
|
|
737
|
-
fetch_multiplier: fetchMultiplier,
|
|
738
|
-
topk_cap_multiplier: topkCapMultiplier,
|
|
739
|
-
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
740
|
-
mmr_penalty_factor: mmrPenaltyFactor,
|
|
741
|
-
phase2_multiplier: phase2Multiplier,
|
|
742
|
-
llm_max_eval: llmMaxEval,
|
|
743
|
-
refine_strategy: "loose" as any,
|
|
744
|
-
refine_medium_chars: refineMediumChars,
|
|
745
|
-
skip_llm_gate: true,
|
|
746
|
-
},
|
|
747
|
-
directory || process.env.OMEM_PROJECT_DIR,
|
|
748
|
-
),
|
|
749
|
-
]);
|
|
750
|
-
if (!recallRes) {
|
|
751
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API", "error", toastDelayMs);
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
shouldRecallRes = recallRes;
|
|
755
|
-
|
|
756
|
-
// build profile block (with TTL check)
|
|
757
|
-
if (profile) {
|
|
758
|
-
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
759
|
-
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 10 * 60 * 1000);
|
|
760
|
-
if (ttlExpired) {
|
|
761
|
-
const built = buildProfileBlock(profile);
|
|
762
|
-
if (built) {
|
|
763
|
-
profileBlock = built.block;
|
|
764
|
-
profileCountText = built.countText;
|
|
765
|
-
profileInjected = true;
|
|
766
|
-
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// write cache for next round
|
|
772
|
-
recallCache.set(input.sessionID, {
|
|
773
|
-
profileBlock,
|
|
774
|
-
recallResult: shouldRecallRes,
|
|
775
|
-
profileData: { countText: profileCountText },
|
|
776
|
-
timestamp: Date.now(),
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// LRU eviction
|
|
780
|
-
if (recallCache.size > 50) {
|
|
781
|
-
let oldestKey: string | null = null;
|
|
782
|
-
let oldestTime = Infinity;
|
|
783
|
-
for (const [k, v] of recallCache) {
|
|
784
|
-
if (v.timestamp < oldestTime) { oldestTime = v.timestamp; oldestKey = k; }
|
|
785
|
-
}
|
|
786
|
-
if (oldestKey) recallCache.delete(oldestKey);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// defensive check
|
|
790
|
-
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
791
|
-
logErr("memoryInjectionHook shouldRecall returned incomplete data", { shouldRecall: shouldRecallRes.should_recall, hasMemories: !!shouldRecallRes.memories });
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
logDebug("memoryInjectionHook cache miss, fetched synchronously", { sessionId: input.sessionID, shouldRecall: shouldRecallRes.should_recall, memCount: shouldRecallRes.memories?.length ?? 0 });
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// ========== unified injection logic (cache hit + cache miss share this) ==========
|
|
799
|
-
if (!shouldRecallRes.should_recall) {
|
|
800
|
-
// no-recall path: inject profile only
|
|
801
|
-
const partsToInject: string[] = [];
|
|
802
|
-
if (profileBlock) partsToInject.push(profileBlock);
|
|
803
|
-
if (partsToInject.length > 0) {
|
|
804
|
-
const injectText = partsToInject.join("\n\n");
|
|
805
|
-
const contextPart: Part = {
|
|
806
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
807
|
-
sessionID: input.sessionID,
|
|
808
|
-
messageID: output.message.id,
|
|
809
|
-
type: "text",
|
|
810
|
-
text: injectText,
|
|
811
|
-
synthetic: true,
|
|
812
|
-
};
|
|
813
|
-
output.parts.unshift(contextPart);
|
|
814
|
-
logDebug("memoryInjectionHook profile injected (no-recall)", { sessionId: input.sessionID });
|
|
815
|
-
}
|
|
816
|
-
injectedSessions.add(input.sessionID);
|
|
817
|
-
const cacheTag = isCacheHit ? " (cached)" : "";
|
|
818
|
-
showToast(tui, `🧠 Profile Injected${cacheTag}`, profileCountText ? `Profile: ${profileCountText} · no recall needed` : "No memory recall needed", "success", toastDelayMs);
|
|
819
|
-
} else {
|
|
820
|
-
const results = shouldRecallRes.memories ?? [];
|
|
821
|
-
const clustered = shouldRecallRes.clustered;
|
|
822
|
-
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
|
|
823
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
824
|
-
logDebug("memoryInjectionHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
|
|
825
|
-
|
|
826
|
-
if (newResults.length === 0) {
|
|
827
|
-
const partsToInject: string[] = [];
|
|
828
|
-
if (profileBlock) partsToInject.push(profileBlock);
|
|
829
|
-
if (partsToInject.length > 0) {
|
|
830
|
-
const injectText = partsToInject.join("\n\n");
|
|
831
|
-
const contextPart: Part = {
|
|
832
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
833
|
-
sessionID: input.sessionID,
|
|
834
|
-
messageID: output.message.id,
|
|
835
|
-
type: "text",
|
|
836
|
-
text: injectText,
|
|
837
|
-
synthetic: true,
|
|
838
|
-
};
|
|
839
|
-
output.parts.unshift(contextPart);
|
|
840
|
-
logDebug("memoryInjectionHook profile injected (dedup)", { sessionId: input.sessionID });
|
|
841
|
-
}
|
|
842
|
-
injectedSessions.add(input.sessionID);
|
|
843
|
-
} else {
|
|
844
|
-
const profileChars = profileInjected ? profileBlock.length : 0;
|
|
845
|
-
const budgetRemaining = maxContentChars - profileChars;
|
|
846
|
-
const itemCount = clustered
|
|
847
|
-
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
848
|
-
: newResults.length;
|
|
849
|
-
const dynamicMaxContentLength = itemCount > 0
|
|
850
|
-
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
851
|
-
: maxContentLength;
|
|
852
|
-
|
|
853
|
-
const block = clustered
|
|
854
|
-
? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
|
|
855
|
-
: buildContextBlock(newResults, dynamicMaxContentLength);
|
|
856
|
-
|
|
857
|
-
const partsToInject: string[] = [];
|
|
858
|
-
if (block) partsToInject.push(block);
|
|
859
|
-
if (block) partsToInject.push(FETCH_POLICY);
|
|
860
|
-
if (profileBlock) partsToInject.push(profileBlock);
|
|
861
|
-
if (isSaveKeyword) partsToInject.push(KEYWORD_NUDGE);
|
|
862
|
-
|
|
863
|
-
if (partsToInject.length > 0) {
|
|
864
|
-
const injectText = partsToInject.join("\n\n");
|
|
865
|
-
const contextPart: Part = {
|
|
866
|
-
id: `prt_cerebro-context-${Date.now()}`,
|
|
867
|
-
sessionID: input.sessionID,
|
|
868
|
-
messageID: output.message.id,
|
|
869
|
-
type: "text",
|
|
870
|
-
text: injectText,
|
|
871
|
-
synthetic: true,
|
|
872
|
-
};
|
|
873
|
-
output.parts.unshift(contextPart);
|
|
874
|
-
logDebug("memoryInjectionHook block injected", {
|
|
875
|
-
sessionId: input.sessionID,
|
|
876
|
-
injectTextLen: injectText.length,
|
|
877
|
-
blockPreview: block?.slice(0, 200),
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
injectedSessions.add(input.sessionID);
|
|
882
|
-
|
|
883
|
-
if (isSaveKeyword) {
|
|
884
|
-
saveKeywordDetectedSessions.delete(input.sessionID);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const newIds = newResults.map((r) => r.memory.id);
|
|
888
|
-
injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
|
|
889
|
-
|
|
890
|
-
const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
|
|
891
|
-
const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;
|
|
892
|
-
const memOther = newResults.length - memDynamic - memStatic;
|
|
893
|
-
|
|
894
|
-
let memCountMsg = "";
|
|
895
|
-
if (memDynamic > 0) memCountMsg += `Dynamic(${memDynamic}) `;
|
|
896
|
-
if (memStatic > 0) memCountMsg += `Static(${memStatic}) `;
|
|
897
|
-
if (memOther > 0) memCountMsg += `Other(${memOther}) `;
|
|
898
|
-
|
|
899
|
-
const categories = categorize(newResults);
|
|
900
|
-
const catSummary = Array.from(categories.entries())
|
|
901
|
-
.map(([label, items]) => `${label}(${items.length})`)
|
|
902
|
-
.join(" · ");
|
|
903
|
-
|
|
904
|
-
let toastTitle: string;
|
|
905
|
-
let toastMessage: string;
|
|
906
|
-
|
|
907
|
-
if (clustered) {
|
|
908
|
-
const clusterCount = clustered.cluster_summaries.length;
|
|
909
|
-
const standaloneCount = clustered.standalone_memories.length;
|
|
910
|
-
toastTitle = `🧠 Context Injected${isCacheHit ? " (cached)" : ""} · ${clusterCount} clusters${standaloneCount > 0 ? ` · ${standaloneCount} standalone` : ""}`;
|
|
911
|
-
toastMessage = profileInjected
|
|
912
|
-
? `Profile: ${profileCountText} · Clustered memory display`
|
|
913
|
-
: `Clustered memory display`;
|
|
914
|
-
} else {
|
|
915
|
-
toastTitle = `🧠 Context Injected${isCacheHit ? " (cached)" : ""} · ${newResults.length} fragments`;
|
|
916
|
-
toastMessage = profileInjected
|
|
917
|
-
? `Profile: ${profileCountText} · Memories: ${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`
|
|
918
|
-
: `${memCountMsg.trim()}${catSummary ? ` · ${catSummary}` : ""}`;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
showToast(tui, toastTitle, toastMessage, "success", toastDelayMs);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// cache miss: fire-and-forget createRecallEvent so web UI shows the record
|
|
926
|
-
if (!isCacheHit) {
|
|
927
|
-
if (shouldRecallRes.should_recall) {
|
|
928
|
-
const results = shouldRecallRes.memories ?? [];
|
|
929
|
-
const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
|
|
930
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
931
|
-
const storedMemoryIds = results.map((r) => r.memory.id);
|
|
932
|
-
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
933
|
-
const maxScore = storedMemoryIds.length > 0
|
|
934
|
-
? Math.max(...(results.map((r) => r.score) ?? [0]))
|
|
935
|
-
: 0;
|
|
936
|
-
const bgBlock = shouldRecallRes.clustered
|
|
937
|
-
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
938
|
-
: buildContextBlock(newResults, maxContentLength);
|
|
939
|
-
const items = [
|
|
940
|
-
...(results.map((r) => ({
|
|
941
|
-
memory_id: r.memory.id, score: r.score,
|
|
942
|
-
refine_relevance: r.refine_relevance, refine_reasoning: r.refine_reasoning, is_kept: true,
|
|
943
|
-
}))),
|
|
944
|
-
...(shouldRecallRes.discarded?.map((d) => ({
|
|
945
|
-
memory_id: d.memory_id, score: d.score,
|
|
946
|
-
refine_relevance: d.refine_relevance, refine_reasoning: d.refine_reasoning, is_kept: false,
|
|
947
|
-
})) ?? []),
|
|
948
|
-
];
|
|
949
|
-
client.createRecallEvent({
|
|
950
|
-
session_id: input.sessionID!, recall_type: "auto", query_text,
|
|
951
|
-
max_score: maxScore, llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
952
|
-
profile_injected: profileInjected,
|
|
953
|
-
kept_count: storedMemoryIds.length, discarded_count: storedDiscardedIds.length,
|
|
954
|
-
injected_count: newResults.length,
|
|
955
|
-
profile_content: profileInjected && profileBlock ? profileBlock : undefined,
|
|
956
|
-
injected_content: bgBlock ?? undefined,
|
|
957
|
-
items: items.length > 0 ? items : undefined,
|
|
958
|
-
}).catch((e: unknown) => {
|
|
959
|
-
logErr("memoryInjectionHook cache-miss createRecallEvent failed", { error: String(e) });
|
|
960
|
-
});
|
|
961
|
-
} else if (profileInjected) {
|
|
962
|
-
client.createRecallEvent({
|
|
963
|
-
session_id: input.sessionID!, recall_type: "auto", query_text,
|
|
964
|
-
max_score: 0, llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
965
|
-
profile_injected: true,
|
|
966
|
-
kept_count: 0, discarded_count: 0, injected_count: 0,
|
|
967
|
-
profile_content: profileBlock || undefined,
|
|
968
|
-
}).catch((e: unknown) => {
|
|
969
|
-
logErr("memoryInjectionHook cache-miss profile-only createRecallEvent failed", { error: String(e) });
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
logDebug("memoryInjectionHook injection complete", { sessionId: input.sessionID, isCacheHit });
|
|
975
|
-
|
|
976
|
-
// ========== Phase B: fire-and-forget async fetch for NEXT round (cache hit only) ==========
|
|
977
|
-
if (isCacheHit) {
|
|
978
|
-
const bgSessionId = input.sessionID;
|
|
979
|
-
const bgQueryText = query_text;
|
|
980
|
-
const bgLastQueryText = last_query_text;
|
|
981
|
-
const bgConversationContext = conversationContext;
|
|
982
|
-
const bgProjectTags = projectTags.length > 0 ? projectTags : undefined;
|
|
983
|
-
const bgDirectory = directory || process.env.OMEM_PROJECT_DIR;
|
|
984
|
-
|
|
985
|
-
Promise.allSettled([
|
|
986
|
-
client.getProfile(),
|
|
987
|
-
client.shouldRecall(
|
|
988
|
-
bgQueryText, bgLastQueryText, bgSessionId,
|
|
989
|
-
similarityThreshold, maxRecallResults,
|
|
990
|
-
bgProjectTags,
|
|
991
|
-
bgConversationContext && bgConversationContext.length > 0 ? bgConversationContext : undefined,
|
|
992
|
-
{
|
|
993
|
-
fetch_multiplier: fetchMultiplier,
|
|
994
|
-
topk_cap_multiplier: topkCapMultiplier,
|
|
995
|
-
mmr_jaccard_threshold: mmrJaccardThreshold,
|
|
996
|
-
mmr_penalty_factor: mmrPenaltyFactor,
|
|
997
|
-
phase2_multiplier: phase2Multiplier,
|
|
998
|
-
llm_max_eval: llmMaxEval,
|
|
999
|
-
refine_strategy: refineStrategy,
|
|
1000
|
-
refine_medium_chars: refineMediumChars,
|
|
1001
|
-
},
|
|
1002
|
-
bgDirectory,
|
|
1003
|
-
),
|
|
1004
|
-
])
|
|
1005
|
-
.then(([profileRes, recallRes]) => {
|
|
1006
|
-
if (recallRes.status === 'rejected') {
|
|
1007
|
-
logErr("memoryInjectionHook shouldRecall failed", { error: String(recallRes.reason) });
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
const profile = profileRes.status === 'fulfilled' ? profileRes.value : null;
|
|
1011
|
-
const shouldRecallRes = recallRes.value;
|
|
1012
|
-
if (!shouldRecallRes) {
|
|
1013
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
logDebug("memoryInjectionHook background fetch complete", {
|
|
1017
|
-
sessionId: bgSessionId,
|
|
1018
|
-
shouldRecall: shouldRecallRes.should_recall,
|
|
1019
|
-
confidence: shouldRecallRes.confidence,
|
|
1020
|
-
memCount: shouldRecallRes.memories?.length ?? 0,
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
if (shouldRecallRes.should_recall && !Array.isArray(shouldRecallRes.memories)) {
|
|
1024
|
-
logErr("memoryInjectionHook shouldRecall returned incomplete data", {
|
|
1025
|
-
shouldRecall: shouldRecallRes.should_recall,
|
|
1026
|
-
hasMemories: !!shouldRecallRes.memories,
|
|
1027
|
-
});
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
let bgProfileBlock = "";
|
|
1032
|
-
let bgProfileCountText = "";
|
|
1033
|
-
let bgProfileInjected = false;
|
|
1034
|
-
|
|
1035
|
-
if (profile) {
|
|
1036
|
-
const lastInjected = profileInjectedSessions.get(bgSessionId);
|
|
1037
|
-
const ttlExpired = !lastInjected || (Date.now() - lastInjected > 10 * 60 * 1000);
|
|
1038
|
-
if (ttlExpired) {
|
|
1039
|
-
const built = buildProfileBlock(profile);
|
|
1040
|
-
if (built) {
|
|
1041
|
-
bgProfileBlock = built.block;
|
|
1042
|
-
bgProfileCountText = built.countText;
|
|
1043
|
-
bgProfileInjected = true;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
recallCache.set(bgSessionId, {
|
|
1049
|
-
profileBlock: bgProfileBlock,
|
|
1050
|
-
recallResult: shouldRecallRes,
|
|
1051
|
-
profileData: { countText: bgProfileCountText },
|
|
1052
|
-
timestamp: Date.now(),
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
if (recallCache.size > 50) {
|
|
1056
|
-
let oldestKey: string | null = null;
|
|
1057
|
-
let oldestTime = Infinity;
|
|
1058
|
-
for (const [k, v] of recallCache) {
|
|
1059
|
-
if (v.timestamp < oldestTime) {
|
|
1060
|
-
oldestTime = v.timestamp;
|
|
1061
|
-
oldestKey = k;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (oldestKey) recallCache.delete(oldestKey);
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (shouldRecallRes.should_recall) {
|
|
1068
|
-
const results = shouldRecallRes.memories ?? [];
|
|
1069
|
-
const existingIds = injectedMemoryIds.get(bgSessionId) ?? new Set<string>();
|
|
1070
|
-
const newResults = results.filter((r) => !existingIds.has(r.memory.id));
|
|
1071
|
-
if (newResults.length > 0) {
|
|
1072
|
-
const newIds = newResults.map((r) => r.memory.id);
|
|
1073
|
-
injectedMemoryIds.set(bgSessionId, new Set([...existingIds, ...newIds]));
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
|
|
1077
|
-
const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
|
|
1078
|
-
const maxScore = storedMemoryIds.length > 0
|
|
1079
|
-
? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
|
|
1080
|
-
: 0;
|
|
1081
|
-
|
|
1082
|
-
const bgBlock = shouldRecallRes.clustered
|
|
1083
|
-
? buildClusteredContextBlock(shouldRecallRes.clustered, maxContentLength)
|
|
1084
|
-
: buildContextBlock(newResults, maxContentLength);
|
|
1085
|
-
const bgInjectedContent = bgBlock ?? undefined;
|
|
1086
|
-
|
|
1087
|
-
const items = [
|
|
1088
|
-
...(shouldRecallRes.memories?.map((r) => ({
|
|
1089
|
-
memory_id: r.memory.id,
|
|
1090
|
-
score: r.score,
|
|
1091
|
-
refine_relevance: r.refine_relevance,
|
|
1092
|
-
refine_reasoning: r.refine_reasoning,
|
|
1093
|
-
is_kept: true,
|
|
1094
|
-
})) ?? []),
|
|
1095
|
-
...(shouldRecallRes.discarded?.map((d) => ({
|
|
1096
|
-
memory_id: d.memory_id,
|
|
1097
|
-
score: d.score,
|
|
1098
|
-
refine_relevance: d.refine_relevance,
|
|
1099
|
-
refine_reasoning: d.refine_reasoning,
|
|
1100
|
-
is_kept: false,
|
|
1101
|
-
})) ?? []),
|
|
1102
|
-
];
|
|
1103
|
-
|
|
1104
|
-
client.createRecallEvent({
|
|
1105
|
-
session_id: bgSessionId,
|
|
1106
|
-
recall_type: "auto",
|
|
1107
|
-
query_text: bgQueryText,
|
|
1108
|
-
max_score: maxScore,
|
|
1109
|
-
llm_confidence: shouldRecallRes.confidence ?? 0,
|
|
1110
|
-
profile_injected: bgProfileInjected,
|
|
1111
|
-
kept_count: storedMemoryIds.length,
|
|
1112
|
-
discarded_count: storedDiscardedIds.length,
|
|
1113
|
-
injected_count: newResults.length,
|
|
1114
|
-
profile_content: bgProfileInjected && bgProfileBlock ? bgProfileBlock : undefined,
|
|
1115
|
-
injected_content: bgInjectedContent,
|
|
1116
|
-
items: items.length > 0 ? items : undefined,
|
|
1117
|
-
}).catch((e: unknown) => {
|
|
1118
|
-
logErr("memoryInjectionHook background createRecallEvent failed", { error: String(e) });
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
})
|
|
1122
|
-
.catch((err: unknown) => {
|
|
1123
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1124
|
-
logErr("memoryInjectionHook background fetch failed", { error: errMsg });
|
|
1125
|
-
if (errMsg.includes("[cerebro]")) {
|
|
1126
|
-
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
1127
|
-
if (cleanMsg.startsWith("500")) {
|
|
1128
|
-
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
1129
|
-
} else if (cleanMsg.includes("timed out")) {
|
|
1130
|
-
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
1131
|
-
}
|
|
1132
|
-
} else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
1133
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
1134
|
-
}
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
} catch (err) {
|
|
1138
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1139
|
-
if (errMsg.includes("[cerebro]")) {
|
|
1140
|
-
const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
|
|
1141
|
-
if (cleanMsg.startsWith("500")) {
|
|
1142
|
-
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
1143
|
-
} else if (cleanMsg.includes("timed out")) {
|
|
1144
|
-
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
1145
|
-
} else {
|
|
1146
|
-
showToast(tui, "🧠 Cerebro Error", cleanMsg.substring(0, 150), "error");
|
|
1147
|
-
}
|
|
1148
|
-
} else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
1149
|
-
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
1150
|
-
} else {
|
|
1151
|
-
showToast(tui, "🧠 Memory Recall Error", errMsg.substring(0, 100), "error");
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
594
|
export function keywordDetectionHook(_client: CerebroClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart", config: Partial<OmemPluginConfig> = {}, agentId?: string) {
|
|
1158
595
|
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
1159
596
|
return async (
|
|
@@ -1297,7 +734,6 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1297
734
|
if (input.sessionID) {
|
|
1298
735
|
sessionMessages.delete(input.sessionID);
|
|
1299
736
|
profileInjectedSessions.delete(input.sessionID);
|
|
1300
|
-
recallCache.delete(input.sessionID);
|
|
1301
737
|
firstMessages.delete(input.sessionID);
|
|
1302
738
|
}
|
|
1303
739
|
return;
|
|
@@ -1329,7 +765,6 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1329
765
|
if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
|
|
1330
766
|
sessionMessages.delete(input.sessionID);
|
|
1331
767
|
profileInjectedSessions.delete(input.sessionID);
|
|
1332
|
-
recallCache.delete(input.sessionID);
|
|
1333
768
|
firstMessages.delete(input.sessionID);
|
|
1334
769
|
} else {
|
|
1335
770
|
const messages = sessionMessages.get(input.sessionID)!;
|
|
@@ -1358,13 +793,10 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1358
793
|
}
|
|
1359
794
|
// Cleanup tracked messages regardless of ingest result
|
|
1360
795
|
sessionMessages.delete(input.sessionID);
|
|
1361
|
-
injectedSessions.delete(input.sessionID);
|
|
1362
796
|
profileInjectedSessions.delete(input.sessionID);
|
|
1363
|
-
recallCache.delete(input.sessionID);
|
|
1364
797
|
firstMessages.delete(input.sessionID);
|
|
1365
798
|
if (input.sessionID) {
|
|
1366
|
-
|
|
1367
|
-
logDebug("compactingHook cleared session pendingToolCalls", { sessionID: input.sessionID, hadPending: deleted });
|
|
799
|
+
logDebug("compactingHook cleared session state", { sessionID: input.sessionID });
|
|
1368
800
|
}
|
|
1369
801
|
// Evict stale injectedMemoryIds if over size cap (200 sessions)
|
|
1370
802
|
if (injectedMemoryIds.size > 200) {
|
|
@@ -1372,156 +804,10 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1372
804
|
}
|
|
1373
805
|
}
|
|
1374
806
|
|
|
1375
|
-
//
|
|
1376
|
-
if (
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const pollProjectName = projectName;
|
|
1380
|
-
const pollProjectPath = projectPath;
|
|
1381
|
-
const pollAgentId = effectiveAgentId;
|
|
1382
|
-
|
|
1383
|
-
let baselineMsgIds: Set<string> = new Set();
|
|
1384
|
-
try {
|
|
1385
|
-
const preResp = await sdkClient.session.messages({ path: { id: pollSessionId } });
|
|
1386
|
-
if (preResp?.data) {
|
|
1387
|
-
baselineMsgIds = new Set(preResp.data.map((m: any) => m.info?.id).filter(Boolean));
|
|
1388
|
-
}
|
|
1389
|
-
logInfo("compactingHook: summary poll starting", { baselineCount: baselineMsgIds.size, sessionId: pollSessionId });
|
|
1390
|
-
} catch (e) {
|
|
1391
|
-
logErr("compactingHook: baseline snapshot failed", { error: String(e) });
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
if (baselineMsgIds.size > 0) {
|
|
1395
|
-
const maxAttempts = 12;
|
|
1396
|
-
const pollInterval = 5000;
|
|
1397
|
-
const COMPACT_MARKER = "[restore checkpointed";
|
|
1398
|
-
|
|
1399
|
-
(async () => {
|
|
1400
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
1401
|
-
await new Promise(r => setTimeout(r, pollInterval));
|
|
1402
|
-
try {
|
|
1403
|
-
const resp = await sdkClient.session.messages({ path: { id: pollSessionId } });
|
|
1404
|
-
if (!resp?.data) continue;
|
|
1405
|
-
|
|
1406
|
-
const currentCount = resp.data.length;
|
|
1407
|
-
logDebug("compactingHook: summary poll tick", {
|
|
1408
|
-
attempt: i + 1, currentCount, baselineCount: baselineMsgIds.size,
|
|
1409
|
-
});
|
|
1410
|
-
|
|
1411
|
-
const compactMsg = resp.data.find((m: any) => {
|
|
1412
|
-
if (m.info?.role !== "user") return false;
|
|
1413
|
-
if (baselineMsgIds.has(m.info?.id)) return false;
|
|
1414
|
-
const textParts = (m.parts || [])
|
|
1415
|
-
.filter((p: any) => p.type === "text" && p.text)
|
|
1416
|
-
.map((p: any) => p.text);
|
|
1417
|
-
return textParts.join("\n").includes(COMPACT_MARKER);
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
if (compactMsg) {
|
|
1421
|
-
const compactIdx = resp.data.findIndex((m: any) => m.info?.id === compactMsg.info?.id);
|
|
1422
|
-
const userTextParts = (compactMsg.parts || [])
|
|
1423
|
-
.filter((p: any) => p.type === "text" && p.text)
|
|
1424
|
-
.map((p: any) => p.text);
|
|
1425
|
-
const userFullText = userTextParts.join("\n").trim();
|
|
1426
|
-
|
|
1427
|
-
logInfo("compactingHook: compact completed detected", {
|
|
1428
|
-
attempt: i + 1, msgId: compactMsg.info?.id,
|
|
1429
|
-
compactIdx, userTextLen: userFullText.length,
|
|
1430
|
-
partsCount: (compactMsg.parts || []).length,
|
|
1431
|
-
partTypes: (compactMsg.parts || []).map((p: any) => p.type),
|
|
1432
|
-
firstPartLen: userTextParts[0]?.length ?? 0,
|
|
1433
|
-
msgsAfterCompact: resp.data.length - compactIdx - 1,
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
if (userFullText.length > 0) {
|
|
1437
|
-
logDebug("compactingHook: compact msg full text", {
|
|
1438
|
-
text: userFullText.substring(0, 500),
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
let summaryText: string | undefined;
|
|
1443
|
-
|
|
1444
|
-
const markerLineIdx = userFullText.indexOf(COMPACT_MARKER);
|
|
1445
|
-
if (markerLineIdx >= 0) {
|
|
1446
|
-
const afterMarker = userFullText.substring(markerLineIdx);
|
|
1447
|
-
const firstNewline = afterMarker.indexOf("\n");
|
|
1448
|
-
const candidate = firstNewline >= 0 ? afterMarker.substring(firstNewline + 1).trim() : "";
|
|
1449
|
-
if (candidate.length > 100) {
|
|
1450
|
-
summaryText = candidate;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
if (!summaryText && compactIdx >= 0) {
|
|
1455
|
-
for (let j = compactIdx + 1; j < resp.data.length; j++) {
|
|
1456
|
-
const msg = resp.data[j];
|
|
1457
|
-
if (msg.info?.role !== "assistant") continue;
|
|
1458
|
-
const assistParts = (msg.parts || [])
|
|
1459
|
-
.filter((p: any) => p.type === "text" && p.text)
|
|
1460
|
-
.map((p: any) => p.text);
|
|
1461
|
-
const assistText = assistParts.join("\n").trim();
|
|
1462
|
-
logDebug("compactingHook: assistant msg after compact", {
|
|
1463
|
-
idx: j, textLen: assistText.length, partTypes: (msg.parts || []).map((p: any) => p.type),
|
|
1464
|
-
preview: assistText.substring(0, 200),
|
|
1465
|
-
});
|
|
1466
|
-
if (assistText.length > 200) {
|
|
1467
|
-
summaryText = assistText;
|
|
1468
|
-
break;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
if (!summaryText && userFullText.length > 100) {
|
|
1474
|
-
summaryText = userFullText;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
if (summaryText) {
|
|
1478
|
-
logInfo("compactingHook: storing compact summary", {
|
|
1479
|
-
summaryLen: summaryText.length, msgId: compactMsg.info?.id,
|
|
1480
|
-
});
|
|
1481
|
-
// Dedup check: 30s cooldown per session+content hash
|
|
1482
|
-
const summaryHash = `${pollSessionId}:${hashString(summaryText)}`;
|
|
1483
|
-
const lastCompacting = compactingSummaryCooldown.get(summaryHash);
|
|
1484
|
-
if (lastCompacting && Date.now() - lastCompacting < 30000) {
|
|
1485
|
-
logDebug("compactingHook summary dedup", { sessionId: pollSessionId });
|
|
1486
|
-
break;
|
|
1487
|
-
}
|
|
1488
|
-
compactingSummaryCooldown.set(summaryHash, Date.now());
|
|
1489
|
-
|
|
1490
|
-
const prefixedSummary = `[Session Summary] ${summaryText}`;
|
|
1491
|
-
try {
|
|
1492
|
-
const result = await client.ingestMessages(
|
|
1493
|
-
[{ role: "user" as const, content: prefixedSummary }],
|
|
1494
|
-
{
|
|
1495
|
-
mode: ingestMode,
|
|
1496
|
-
tags: [...containerTags, "auto-capture", "compact-summary"],
|
|
1497
|
-
sessionId: pollEffectiveSessionId,
|
|
1498
|
-
projectName: pollProjectName,
|
|
1499
|
-
agentId: pollAgentId,
|
|
1500
|
-
projectPath: pollProjectPath,
|
|
1501
|
-
},
|
|
1502
|
-
);
|
|
1503
|
-
logInfo("compactingHook: compact summary store result", {
|
|
1504
|
-
result: result === null ? "null(blocked)" : "ok",
|
|
1505
|
-
});
|
|
1506
|
-
if (result !== null) {
|
|
1507
|
-
showToast(tui, "📦 Compact Summary Stored", "Session summary archived to memory", "success");
|
|
1508
|
-
}
|
|
1509
|
-
} catch (e) {
|
|
1510
|
-
logErr("compactingHook: compact summary store failed", { error: String(e) });
|
|
1511
|
-
}
|
|
1512
|
-
} else {
|
|
1513
|
-
logInfo("compactingHook: no summary text found after compact marker", {
|
|
1514
|
-
userTextLen: userFullText.length, compactIdx,
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
break;
|
|
1518
|
-
}
|
|
1519
|
-
} catch (e) {
|
|
1520
|
-
logErr("compactingHook: summary poll error", { error: String(e), attempt: i + 1 });
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
})();
|
|
1524
|
-
}
|
|
807
|
+
// After compacting, clear profile TTL so next autoRecallHook re-injects profile
|
|
808
|
+
if (input.sessionID) {
|
|
809
|
+
profileInjectedSessions.delete(input.sessionID);
|
|
810
|
+
logDebug("compactingHook cleared profile TTL for re-injection", { sessionID: input.sessionID });
|
|
1525
811
|
}
|
|
1526
812
|
};
|
|
1527
813
|
}
|
|
@@ -1639,77 +925,137 @@ export function autocontinueHook(
|
|
|
1639
925
|
const processedMessageIds = new Set<string>();
|
|
1640
926
|
const pluginStartTime = Date.now();
|
|
1641
927
|
|
|
1642
|
-
|
|
1643
|
-
|
|
928
|
+
export function sessionIdleHook(
|
|
929
|
+
cerebroClient: CerebroClient,
|
|
930
|
+
containerTags: string[],
|
|
931
|
+
tui: any,
|
|
932
|
+
sdkClient: any,
|
|
933
|
+
ingestMode: "smart" | "raw" = "smart",
|
|
934
|
+
threshold: number = 0,
|
|
935
|
+
getMainSessionId?: () => string | undefined,
|
|
936
|
+
isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
|
|
937
|
+
agentId?: string,
|
|
938
|
+
config: Partial<OmemPluginConfig> = {},
|
|
939
|
+
onAgentResolved?: (name: string) => void,
|
|
940
|
+
directory?: string,
|
|
941
|
+
) {
|
|
942
|
+
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
943
|
+
let isCapturing = false;
|
|
944
|
+
|
|
945
|
+
async function handleSummaryCapture(props: any) {
|
|
946
|
+
const info = props?.info;
|
|
947
|
+
if (!info) return;
|
|
948
|
+
if (info.role !== "assistant" || !info.summary || !info.finish) return;
|
|
949
|
+
|
|
950
|
+
const sessionID = info.sessionID;
|
|
951
|
+
if (!sessionID) return;
|
|
952
|
+
|
|
953
|
+
if (summarizedSessions.has(sessionID)) return;
|
|
954
|
+
summarizedSessions.add(sessionID);
|
|
1644
955
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
if (config.soulWhisper?.enabled === false) {
|
|
1648
|
-
logDebug("soulWhisperToolTracker disabled by config", { tool: input.tool });
|
|
956
|
+
if (!sdkClient) {
|
|
957
|
+
logInfo("handleSummaryCapture skipped: no sdkClient", { sessionID });
|
|
1649
958
|
return;
|
|
1650
959
|
}
|
|
1651
960
|
|
|
1652
|
-
|
|
1653
|
-
const toolName = input.tool;
|
|
961
|
+
logInfo("handleSummaryCapture triggered", { sessionID });
|
|
1654
962
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
963
|
+
if (getMainSessionId) {
|
|
964
|
+
const mainId = getMainSessionId();
|
|
965
|
+
if (mainId && sessionID !== mainId) {
|
|
966
|
+
logInfo("handleSummaryCapture: non-main session skipped", { sessionID, mainSessionId: mainId });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
1659
969
|
}
|
|
1660
970
|
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
971
|
+
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
972
|
+
const policy = resolveAgentPolicy(effectiveAgentId, config);
|
|
973
|
+
if (policy !== "readwrite") {
|
|
974
|
+
logInfo("handleSummaryCapture blocked by policy", { agentId: effectiveAgentId, policy });
|
|
1665
975
|
return;
|
|
1666
976
|
}
|
|
1667
977
|
|
|
1668
|
-
|
|
1669
|
-
let sessionMap = pendingToolCalls.get(sid);
|
|
1670
|
-
if (!sessionMap) {
|
|
1671
|
-
sessionMap = new Map();
|
|
1672
|
-
pendingToolCalls.set(sid, sessionMap);
|
|
1673
|
-
}
|
|
1674
|
-
sessionMap.set(input.callID, { toolName, timestamp: Date.now() });
|
|
1675
|
-
logDebug("soulWhisperToolTracker recorded", { tool: toolName, callID: input.callID, sessionID: sid, totalSessions: pendingToolCalls.size, sessionCallCount: sessionMap.size });
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
978
|
+
if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID)) return;
|
|
1678
979
|
|
|
1679
|
-
|
|
1680
|
-
|
|
980
|
+
try {
|
|
981
|
+
const resp = await sdkClient.session.messages({ path: { id: sessionID } });
|
|
982
|
+
const messages = resp?.data ?? resp;
|
|
1681
983
|
|
|
1682
|
-
|
|
984
|
+
const summaryMsg = (messages as Array<{ info: any; parts?: Array<{ type: string; text?: string }> }>).find((m) =>
|
|
985
|
+
m.info?.role === "assistant" && m.info?.summary === true
|
|
986
|
+
);
|
|
1683
987
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
988
|
+
if (!summaryMsg?.parts) {
|
|
989
|
+
logInfo("handleSummaryCapture: no summary parts found", { sessionID });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
1689
992
|
|
|
1690
|
-
|
|
993
|
+
const textParts = summaryMsg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text);
|
|
994
|
+
const summaryContent = textParts.join("\n").trim();
|
|
1691
995
|
|
|
1692
|
-
|
|
1693
|
-
}
|
|
996
|
+
if (!summaryContent || summaryContent.length < 100) {
|
|
997
|
+
logInfo("handleSummaryCapture: summary too short", { sessionID, length: summaryContent?.length ?? 0 });
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1694
1000
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
) {
|
|
1709
|
-
|
|
1710
|
-
|
|
1001
|
+
const effectiveSessionId = getMainSessionId?.() || sessionID;
|
|
1002
|
+
|
|
1003
|
+
let projectName: string | undefined;
|
|
1004
|
+
let projectPath: string | undefined;
|
|
1005
|
+
try {
|
|
1006
|
+
const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
|
|
1007
|
+
projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
|
|
1008
|
+
projectName = sessionInfo?.data?.directory
|
|
1009
|
+
? await detectProjectName(sessionInfo.data.directory)
|
|
1010
|
+
: undefined;
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
logErr("handleSummaryCapture detectProjectName failed", { error: String(e) });
|
|
1013
|
+
}
|
|
1014
|
+
if (!projectPath) {
|
|
1015
|
+
projectPath = directory || process.env.OMEM_PROJECT_DIR;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const prefixedSummary = `[Session Summary] ${summaryContent}`;
|
|
1019
|
+
const result = await cerebroClient.ingestMessages(
|
|
1020
|
+
[{ role: "user" as const, content: prefixedSummary }],
|
|
1021
|
+
{
|
|
1022
|
+
mode: ingestMode,
|
|
1023
|
+
tags: [...containerTags, "auto-capture", "compact-summary"],
|
|
1024
|
+
sessionId: effectiveSessionId,
|
|
1025
|
+
projectName,
|
|
1026
|
+
agentId: effectiveAgentId,
|
|
1027
|
+
projectPath,
|
|
1028
|
+
},
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
logInfo("handleSummaryCapture store result", { result: result === null ? "null(blocked)" : "ok" });
|
|
1032
|
+
if (result !== null) {
|
|
1033
|
+
showToast(tui, "📦 Compact Summary Stored", "Session summary archived", "success");
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
logErr("handleSummaryCapture failed", { error: String(err) });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1711
1039
|
|
|
1712
1040
|
return async (input: { event: { type: string; properties?: any } }) => {
|
|
1041
|
+
if (input.event.type === "message.updated") {
|
|
1042
|
+
await handleSummaryCapture(input.event.properties);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (input.event.type === "session.deleted") {
|
|
1047
|
+
const sessionInfo = input.event.properties?.info;
|
|
1048
|
+
const sid = sessionInfo?.id;
|
|
1049
|
+
if (sid) {
|
|
1050
|
+
summarizedSessions.delete(sid);
|
|
1051
|
+
sessionMessages.delete(sid);
|
|
1052
|
+
profileInjectedSessions.delete(sid);
|
|
1053
|
+
firstMessages.delete(sid);
|
|
1054
|
+
logDebug("sessionIdleHook: session.deleted cleanup", { sessionID: sid });
|
|
1055
|
+
}
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1713
1059
|
if (input.event.type !== "session.idle") return;
|
|
1714
1060
|
|
|
1715
1061
|
logDebug("sessionIdleHook event.properties dump", { keys: Object.keys(input.event.properties || {}), raw: JSON.stringify(input.event.properties).substring(0, 2000) });
|
|
@@ -1817,8 +1163,6 @@ export function sessionIdleHook(
|
|
|
1817
1163
|
} finally {
|
|
1818
1164
|
isCapturing = false;
|
|
1819
1165
|
idleTimeout = null;
|
|
1820
|
-
const deleted = pendingToolCalls.delete(sessionID);
|
|
1821
|
-
if (deleted) logDebug("sessionIdleHook cleared session pendingToolCalls", { sessionID, hadPending: deleted });
|
|
1822
1166
|
}
|
|
1823
1167
|
}, 10000);
|
|
1824
1168
|
};
|