@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/src/hooks.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Model, UserMessage, Part } from "@opencode-ai/sdk";
2
- import type { CerebroClient, SearchResult, ShouldRecallResponse } from "./client.js";
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 injectedSessions = new Set<string>();
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 (before query_text check, but injection deferred to after context) ---
360
- const profile = await client.getProfile();
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
- let profileBlock = "";
364
- const lastInjected = profileInjectedSessions.get(input.sessionID);
365
- const ttlExpired = !lastInjected || (Date.now() - lastInjected > 10 * 60 * 1000);
366
- const isFirstInjection = !lastInjected;
367
- if (profile && ttlExpired) {
368
- const prefs = ((profile as any)?.static_facts ?? [])
369
- .filter((sf: any) => {
370
- const t: string[] = sf.tags ?? [];
371
- return t.includes("preferences");
372
- })
373
- .map((sf: any) => sf.l2_content ?? sf.content ?? "")
374
- .filter(Boolean);
375
- const profileLines = prefs.length > 0
376
- ? prefs.map((c: string) => ` · ${c}`).join("\n")
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 && isFirstInjection) {
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 && isFirstInjection) {
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
- const deleted = pendingToolCalls.delete(input.sessionID);
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
- // Phase 2: compact inserts "[restore checkpointed" user message poll for that marker
1376
- if (sdkClient && input.sessionID) {
1377
- const pollSessionId = input.sessionID;
1378
- const pollEffectiveSessionId = effectiveSessionId;
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
- // ── Soul Whisper: pending tool call tracking (per-session isolation) ──
1643
- export const pendingToolCalls = new Map<string, Map<string, { toolName: string; timestamp: number }>>();
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
- export function soulWhisperToolTracker(config: OmemPluginConfig) {
1646
- return async (input: { tool: string; sessionID: string; callID: string }, _output: { args: any }) => {
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
- const sw = config.soulWhisper;
1653
- const toolName = input.tool;
961
+ logInfo("handleSummaryCapture triggered", { sessionID });
1654
962
 
1655
- const excludeTools = sw?.excludeTools ?? [];
1656
- if (excludeTools.includes(toolName)) {
1657
- logDebug("soulWhisperToolTracker excluded", { tool: toolName });
1658
- return;
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 includeTools = sw?.tools ?? ["*"];
1662
- const isWildcard = includeTools.includes("*");
1663
- if (!isWildcard && !includeTools.includes(toolName)) {
1664
- logDebug("soulWhisperToolTracker not in whitelist", { tool: toolName, whitelist: includeTools });
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
- const sid = input.sessionID || "_default";
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
- export function buildWhisperText(toolNames: string[], maxToolNames: number): string | null {
1680
- if (toolNames.length === 0) return null;
980
+ try {
981
+ const resp = await sdkClient.session.messages({ path: { id: sessionID } });
982
+ const messages = resp?.data ?? resp;
1681
983
 
1682
- const lines: string[] = ["<cerebro-memory-activation>"];
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
- if (toolNames.length <= maxToolNames) {
1685
- lines.push(`Before using ${toolNames.join(", ")}, memory_search() may surface relevant past decisions or patterns. Brief recall → better outcomes.`);
1686
- } else {
1687
- 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.");
1688
- }
988
+ if (!summaryMsg?.parts) {
989
+ logInfo("handleSummaryCapture: no summary parts found", { sessionID });
990
+ return;
991
+ }
1689
992
 
1690
- lines.push("</cerebro-memory-activation>");
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
- return lines.join("\n");
1693
- }
996
+ if (!summaryContent || summaryContent.length < 100) {
997
+ logInfo("handleSummaryCapture: summary too short", { sessionID, length: summaryContent?.length ?? 0 });
998
+ return;
999
+ }
1694
1000
 
1695
- export function sessionIdleHook(
1696
- cerebroClient: CerebroClient,
1697
- _containerTags: string[],
1698
- tui: any,
1699
- sdkClient: any,
1700
- _ingestMode: "smart" | "raw" = "smart",
1701
- threshold: number = 0,
1702
- getMainSessionId?: () => string | undefined,
1703
- isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
1704
- agentId?: string,
1705
- config: Partial<OmemPluginConfig> = {},
1706
- onAgentResolved?: (name: string) => void,
1707
- directory?: string,
1708
- ) {
1709
- let idleTimeout: ReturnType<typeof setTimeout> | null = null;
1710
- let isCapturing = false;
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
  };